This document provides a complete API reference for the CoseSignTool plugin system.
All plugin interfaces and base classes are contained in the CoseSignTool.Abstractions namespace.
- Target Framework: .NET 8.0
- Assembly Naming: Must end with
.Plugin.dllfor automatic discovery - Project Naming: Must end with
.Plugin.csprojfor automatic CI/CD packaging - Location: Must be placed in the
pluginssubdirectory of CoseSignTool
For full automatic integration with CoseSignTool:
<ProjectName>.Plugin.csproj
Examples:
- ✅
YourCompany.CustomSigning.Plugin.csproj→ Automatically packaged in releases - ✅
AzureKeyVault.Integration.Plugin.csproj→ Automatically packaged in releases - ❌
CustomSigningTool.csproj→ NOT automatically packaged
<AssemblyName>YourCompany.CustomSigning.Plugin</AssemblyName>Results in: YourCompany.CustomSigning.Plugin.dll → Automatically discovered at runtime
- ✅ Zero CI/CD Maintenance: No manual updates to build scripts
- ✅ Automatic Packaging: Included in all releases automatically
- ✅ Automatic Discovery: Commands appear in CoseSignTool help
- ✅ Future-Proof: Works with any number of plugins
Main interface that all plugins must implement.
public interface ICoseSignToolPlugin
{
string Name { get; }
string Version { get; }
string Description { get; }
IEnumerable<IPluginCommand> Commands { get; }
void Initialize(IConfiguration? configuration = null);
}| Property | Type | Description |
|---|---|---|
Name |
string |
Display name of the plugin (e.g., "Microsoft's Signing Transparency") |
Version |
string |
Version string (semantic versioning recommended) |
Description |
string |
Brief description shown in help output |
Commands |
IEnumerable<IPluginCommand> |
Collection of commands provided by this plugin |
| Method | Description |
|---|---|
Initialize(IConfiguration?) |
Called once when plugin is loaded. Use for setup, validation, and configuration |
Example Implementation:
public class MyPlugin : ICoseSignToolPlugin
{
public string Name => "My Service Plugin";
public string Version => Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0";
public string Description => "Integration with My Service";
public IEnumerable<IPluginCommand> Commands => new[] { new MyCommand() };
public void Initialize(IConfiguration? configuration = null)
{
// Plugin initialization logic
}
}Interface for individual commands within a plugin.
public interface IPluginCommand
{
string Name { get; }
string Description { get; }
string Usage { get; }
IDictionary<string, string> Options { get; }
Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default);
}| Property | Type | Description |
|---|---|---|
Name |
string |
Command name used on command line (e.g., "register", "verify") |
Description |
string |
Short description for help output |
Usage |
string |
Detailed usage instructions with examples |
Options |
IDictionary<string, string> |
Maps command-line options to configuration keys |
| Method | Return Type | Description |
|---|---|---|
ExecuteAsync(IConfiguration, CancellationToken) |
Task<PluginExitCode> |
Executes the command with provided configuration |
Example Implementation:
public class MyCommand : IPluginCommand
{
public string Name => "mycommand";
public string Description => "Does something useful";
public string Usage => "Usage: CoseSignTool mycommand --option value";
public IDictionary<string, string> Options => new Dictionary<string, string>
{
["--option"] = "option",
["-o"] = "option"
};
public async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default)
{
// Command implementation
return PluginExitCode.Success;
}
}Abstract base class providing common functionality for plugin commands.
public abstract class PluginCommandBase : IPluginCommand
{
public abstract string Name { get; }
public abstract string Description { get; }
public abstract string Usage { get; }
public abstract IDictionary<string, string> Options { get; }
public abstract Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default);
protected static string GetRequiredValue(IConfiguration configuration, string key);
protected static string? GetOptionalValue(IConfiguration configuration, string key, string? defaultValue = null);
}| Method | Description |
|---|---|
GetRequiredValue(IConfiguration, string) |
Gets a required configuration value, throws if missing |
GetOptionalValue(IConfiguration, string, string?) |
Gets an optional configuration value with default |
SetLogger(IPluginLogger) |
Sets the logger (called automatically by CLI before ExecuteAsync) |
| Property | Type | Description |
|---|---|---|
Logger |
IPluginLogger |
Logger instance for diagnostic output (set automatically by CLI) |
Example Usage:
public class MyCommand : PluginCommandBase
{
public override async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken)
{
try
{
// Logger is already configured by CLI - just use it!
Logger.LogVerbose("Starting operation");
string required = GetRequiredValue(configuration, "endpoint");
string optional = GetOptionalValue(configuration, "timeout", "30") ?? "30";
Logger.LogInformation($"Connecting to {required}");
// Command logic here
Logger.LogInformation("Operation completed successfully");
return PluginExitCode.Success;
}
catch (ArgumentNullException ex)
{
Logger.LogError($"Missing required option: {ex.ParamName}");
return PluginExitCode.MissingRequiredOption;
}
catch (Exception ex)
{
Logger.LogError($"Operation failed: {ex.Message}");
Logger.LogException(ex);
return PluginExitCode.UnknownError;
}
}
}Interface for diagnostic logging in plugins.
public interface IPluginLogger
{
LogLevel Level { get; }
void LogInformation(string message);
void LogVerbose(string message);
void LogWarning(string message);
void LogError(string message);
void LogException(Exception exception);
}public enum LogLevel
{
Quiet = 0, // Only errors
Normal = 1, // Info + errors
Verbose = 2 // Everything including debug info
}| Method | LogLevel Required | Description |
|---|---|---|
LogInformation(string) |
Normal or higher | Status messages and results |
LogVerbose(string) |
Verbose only | Detailed diagnostic information |
LogWarning(string) |
Normal or higher | Warning messages (yellow in console) |
LogError(string) |
Always shown | Error messages (red in console) |
LogException(Exception) |
Verbose: full stack trace Normal: message only |
Exception details |
Default implementation that outputs to console with colored text.
public class ConsolePluginLogger : IPluginLogger
{
public ConsolePluginLogger(LogLevel level = LogLevel.Normal);
}Features:
- Colored output (yellow warnings, red errors)
- Verbosity-aware filtering
- Exception formatting with stack traces in verbose mode
Example Usage:
public class MyCommand : PluginCommandBase
{
public override async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken)
{
// Configure logging (automatically creates ConsolePluginLogger)
ConfigureLogging(configuration);
Logger.LogVerbose("This only shows with --verbose");
Logger.LogInformation("This shows in normal and verbose modes");
Logger.LogWarning("This is a warning (yellow text)");
Logger.LogError("This always shows (red text)");
try
{
// Operation that might fail
}
catch (Exception ex)
{
Logger.LogException(ex); // Full stack trace in --verbose
}
return PluginExitCode.Success;
}
}Universal Logging Flags (automatically available for all plugins):
The CLI infrastructure automatically adds these flags to every plugin command:
--verbose, -v Enable verbose logging output
--quiet, -q Suppress all non-error output
--verbosity Set log level explicitly (verbose, normal, quiet)You do NOT need to:
- Add these to your command's
Optionsdictionary - Parse these flags in your ExecuteAsync method
- Call ConfigureLogging() - the CLI handles this automatically
The CLI automatically:
- Parses logging flags from command line
- Creates appropriate logger instance (ConsolePluginLogger with correct level)
- Injects logger via SetLogger() before calling ExecuteAsync
Users can control logging for any plugin command:
# Normal mode (default)
CoseSignTool mycommand --option value
# Verbose mode
CoseSignTool mycommand --option value --verbose
# Quiet mode
CoseSignTool mycommand --option value --quietRepresents the result of executing a plugin command.
public enum PluginExitCode
{
Success = 0,
HelpRequested = 1,
MissingRequiredOption = 2,
UnknownArgument = 3,
InvalidArgumentValue = 4,
MissingArgumentValue = 5,
UserSpecifiedFileNotFound = 6,
UnknownError = 10
}| Value | Code | Description | When to Use |
|---|---|---|---|
Success |
0 | Command completed successfully | Normal completion |
HelpRequested |
1 | User requested help | When --help or similar is detected |
MissingRequiredOption |
2 | Required option missing | Required command-line option not provided |
UnknownArgument |
3 | Unrecognized argument | Invalid command-line option |
InvalidArgumentValue |
4 | Invalid argument value | Option value is invalid format/range |
MissingArgumentValue |
5 | Missing argument value | Option provided without required value |
UserSpecifiedFileNotFound |
6 | File not found | User-specified file doesn't exist |
UnknownError |
10 | Unexpected error | Unhandled exceptions or unexpected conditions |
Provides functionality to discover and load plugins.
public static class PluginLoader
{
public static IEnumerable<ICoseSignToolPlugin> DiscoverPlugins(string pluginDirectory);
public static void ValidatePluginDirectory(string pluginDirectory);
public static ICoseSignToolPlugin? LoadPlugin(string assemblyPath);
public static ICoseSignToolPlugin? LoadPlugin(Assembly assembly);
}| Method | Description |
|---|---|
DiscoverPlugins(string) |
Discovers all plugins in the specified directory |
ValidatePluginDirectory(string) |
Validates directory is authorized for plugin loading |
LoadPlugin(string) |
Loads a plugin from an assembly file path |
LoadPlugin(Assembly) |
Loads a plugin from an Assembly object |
Security Note: ValidatePluginDirectory throws UnauthorizedAccessException if the directory is not the authorized plugins subdirectory.
Plugin commands receive configuration through the standard .NET IConfiguration interface.
Common Patterns:
// Get string value
string? value = configuration["key"];
// Get with indexer
string? value = configuration["section:subsection"];
// Check if exists
bool exists = configuration.GetSection("key").Exists();
// Get typed value
int timeout = configuration.GetValue<int>("timeout", 30);The Options dictionary maps command-line arguments to configuration keys:
public IDictionary<string, string> Options => new Dictionary<string, string>
{
["--endpoint"] = "endpoint", // --endpoint value -> configuration["endpoint"]
["-e"] = "endpoint", // -e value -> configuration["endpoint"] (alias)
["--timeout"] = "timeout", // --timeout value -> configuration["timeout"]
["--verbose"] = "verbose" // --verbose -> configuration["verbose"] = "true"
};public async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// Command logic here
return PluginExitCode.Success;
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Operation was cancelled");
return PluginExitCode.UnknownError;
}
catch (ArgumentNullException ex)
{
Console.Error.WriteLine($"Missing required option: {ex.ParamName}");
return PluginExitCode.MissingRequiredOption;
}
catch (FileNotFoundException ex)
{
Console.Error.WriteLine($"File not found: {ex.FileName}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
catch (ArgumentException ex)
{
Console.Error.WriteLine($"Invalid argument: {ex.Message}");
return PluginExitCode.InvalidArgumentValue;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Unexpected error: {ex.Message}");
return PluginExitCode.UnknownError;
}
}// Required parameter validation
string endpoint = GetRequiredValue(configuration, "endpoint");
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri))
{
Console.Error.WriteLine($"Invalid endpoint URL: {endpoint}");
return PluginExitCode.InvalidArgumentValue;
}
// File existence validation
string filePath = GetRequiredValue(configuration, "file");
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"File not found: {filePath}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
// Numeric validation
string timeoutStr = GetOptionalValue(configuration, "timeout", "30") ?? "30";
if (!int.TryParse(timeoutStr, out int timeout) || timeout <= 0)
{
Console.Error.WriteLine($"Invalid timeout value: {timeoutStr}");
return PluginExitCode.InvalidArgumentValue;
}Always support cancellation in async operations:
// Check before expensive operations
cancellationToken.ThrowIfCancellationRequested();
// Pass to async methods
await httpClient.SendAsync(request, cancellationToken);
// Use in loops
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessItem(item, cancellationToken);
}Properly dispose of resources:
public async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken)
{
using var httpClient = new HttpClient();
using var fileStream = File.OpenRead(filePath);
try
{
// Use resources
return PluginExitCode.Success;
}
finally
{
// Resources automatically disposed
}
}Use appropriate output streams:
// Normal output
Console.WriteLine("Operation completed successfully");
// Error output
Console.Error.WriteLine("Error: Invalid input");
// Diagnostic output (when verbose)
if (verbose)
{
Console.WriteLine($"Processing file: {fileName}");
}- Validate all inputs: Never trust user-provided data
- Sanitize file paths: Prevent directory traversal attacks
- Limit file access: Only access files explicitly provided by user
- Handle secrets carefully: Don't log sensitive information
// Safe file path handling
string userPath = GetRequiredValue(configuration, "file");
string fullPath = Path.GetFullPath(userPath);
// Validate the file is in expected location
if (!fullPath.StartsWith(allowedDirectory, StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine("File access denied");
return PluginExitCode.InvalidArgumentValue;
}Extract version information safely:
public string Version =>
System.Reflection.Assembly.GetExecutingAssembly()
.GetName()
.Version?
.ToString() ?? "1.0.0";- Target .NET 8.0
- Use compatible versions of Microsoft.Extensions.* packages
- Avoid conflicts with CoseSignTool dependencies
- Package additional dependencies with your plugin
[Test]
public async Task ExecuteAsync_ValidInput_ReturnsSuccess()
{
// Arrange
var command = new MyCommand();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["endpoint"] = "https://example.com",
["timeout"] = "30"
})
.Build();
// Act
var result = await command.ExecuteAsync(configuration);
// Assert
Assert.AreEqual(PluginExitCode.Success, result);
}[Test]
public void PluginLoader_DiscoverPlugins_FindsTestPlugin()
{
// Arrange
string pluginDirectory = CreateTestPluginDirectory();
// Act
var plugins = PluginLoader.DiscoverPlugins(pluginDirectory);
// Assert
Assert.IsTrue(plugins.Any());
Assert.AreEqual("Test Plugin", plugins.First().Name);
}If breaking changes are introduced, migration steps will be documented here.
No deprecated APIs currently. Check this section for future deprecations.
Plugin not discovered:
- Assembly name must end with
.Plugin.dll - Must implement
ICoseSignToolPlugin - Must be in
pluginsdirectory
Runtime errors:
- Check .NET 8.0 compatibility
- Verify all dependencies are available
- Check console output for detailed error messages
Command not working:
- Verify
Optionsdictionary mapping - Check command name uniqueness
- Validate input parameters
For more troubleshooting information, see the main Plugins.md documentation.