This guide will help you create your first CoseSignTool plugin in just a few minutes.
- .NET 8.0 SDK
- CoseSignTool source code or binaries
- A code editor (Visual Studio, VS Code, etc.)
Create a new class library project with the correct naming convention:
# IMPORTANT: Use .Plugin.csproj naming for automatic CI/CD packaging
dotnet new classlib -n MyFirst.Plugin
cd MyFirst.PluginUpdate the project file (MyFirst.Plugin.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>MyFirst.Plugin</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CoseSignTool.Abstractions\CoseSignTool.Abstractions.csproj" />
</ItemGroup>
</Project>For automatic CI/CD packaging and deployment, your project must follow these conventions:
✅ Project File: <Name>.Plugin.csproj (enables automatic CI/CD packaging)
✅ Assembly Name: <Name>.Plugin.dll (enables runtime discovery)
MyFirst.Plugin.csproj→ ✅ Automatically packaged in CI/CDAzureVault.Plugin.csproj→ ✅ Automatically packaged in CI/CDCustomSigning.Plugin.csproj→ ✅ Automatically packaged in CI/CDMyFirstPlugin.csproj→ ❌ NOT automatically packaged (missing.Pluginsuffix)
Replace the contents of Class1.cs with:
using CoseSignTool.Abstractions;
using Microsoft.Extensions.Configuration;
namespace MyFirstPlugin;
public class MyFirstPlugin : ICoseSignToolPlugin
{
private readonly List<IPluginCommand> _commands;
public MyFirstPlugin()
{
_commands = new List<IPluginCommand>
{
new HelloCommand()
};
}
public string Name => "My First Plugin";
public string Version => "1.0.0";
public string Description => "A simple example plugin for CoseSignTool";
public IEnumerable<IPluginCommand> Commands => _commands;
public void Initialize(IConfiguration? configuration = null)
{
// Plugin initialization code here
Console.WriteLine("My First Plugin initialized!");
}
}Add a new file HelloCommand.cs:
using CoseSignTool.Abstractions;
using Microsoft.Extensions.Configuration;
namespace MyFirstPlugin;
public class HelloCommand : PluginCommandBase
{
public override string Name => "hello";
public override string Description => "Says hello with optional name parameter";
public override string Usage => @"
hello - Says hello
Usage:
CoseSignTool hello [--name <name>]
Options:
--name Name to greet (optional, default: 'World')
";
public override IDictionary<string, string> Options => new Dictionary<string, string>
{
["--name"] = "name"
};
public override async Task<PluginExitCode> ExecuteAsync(
IConfiguration configuration,
CancellationToken cancellationToken = default)
{
try
{
// Check for cancellation
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(cancellationToken);
}
// Get the name parameter (optional)
string name = GetOptionalValue(configuration, "name", "World") ?? "World";
// Simple greeting
Console.WriteLine($"Hello, {name}!");
// Simulate some async work
await Task.Delay(100, cancellationToken);
Console.WriteLine("Plugin execution completed successfully.");
return PluginExitCode.Success;
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Operation was cancelled");
return PluginExitCode.UnknownError;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return PluginExitCode.UnknownError;
}
}
}Build your plugin:
dotnet buildEnhanced Subdirectory Deployment (Recommended):
- Create a subdirectory for your plugin:
# Windows
mkdir "C:\path\to\CoseSignTool\plugins\MyFirst.Plugin"
# Linux/macOS
mkdir -p /path/to/CoseSignTool/plugins/MyFirst.Plugin- Copy the built assembly and dependencies to the subdirectory:
# Windows
copy bin\Debug\net8.0\*.* "C:\path\to\CoseSignTool\plugins\MyFirst.Plugin\"
# Linux/macOS
cp bin/Debug/net8.0/* /path/to/CoseSignTool/plugins/MyFirst.Plugin/Legacy Flat Deployment (Backward Compatibility):
- Copy just the plugin DLL to the main plugins directory:
# Windows
copy bin\Debug\net8.0\MyFirst.Plugin.dll "C:\path\to\CoseSignTool\plugins\"
# Linux/macOS
cp bin/Debug/net8.0/MyFirst.Plugin.dll /path/to/CoseSignTool/plugins/Benefits of Subdirectory Deployment:
- Dependency Isolation: Your plugin dependencies won't conflict with other plugins
- Self-Contained: Easy to distribute and manage as a complete unit
- Version Independence: Use any dependency versions without worrying about conflicts
Run CoseSignTool to see your plugin in the help:
CoseSignTool --helpYou should see your hello command listed under "Plugin Commands".
Test your command:
# Basic usage
CoseSignTool hello
# With name parameter
CoseSignTool hello --name "Developer"Let's extend the hello command to work with files. Update HelloCommand.cs:
public override string Usage => @"
hello - Says hello and optionally reads from a file
Usage:
CoseSignTool hello [--name <name>] [--input <file>] [--output <file>]
Options:
--name Name to greet (optional, default: 'World')
--input Input file to read greeting from
--output Output file to write greeting to
";
public override IDictionary<string, string> Options => new Dictionary<string, string>
{
["--name"] = "name",
["--input"] = "input",
["--output"] = "output"
};
public override async Task<PluginExitCode> ExecuteAsync(
IConfiguration configuration,
CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string name = GetOptionalValue(configuration, "name", "World") ?? "World";
string? inputFile = GetOptionalValue(configuration, "input");
string? outputFile = GetOptionalValue(configuration, "output");
// Read from input file if specified
if (!string.IsNullOrEmpty(inputFile))
{
if (!File.Exists(inputFile))
{
Console.Error.WriteLine($"Input file not found: {inputFile}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
string fileContent = await File.ReadAllTextAsync(inputFile, cancellationToken);
name = fileContent.Trim();
}
string greeting = $"Hello, {name}! Greetings from My First Plugin.";
// Write to output file if specified
if (!string.IsNullOrEmpty(outputFile))
{
await File.WriteAllTextAsync(outputFile, greeting, cancellationToken);
Console.WriteLine($"Greeting written to: {outputFile}");
}
else
{
Console.WriteLine(greeting);
}
return PluginExitCode.Success;
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Operation was cancelled");
return PluginExitCode.UnknownError;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return PluginExitCode.UnknownError;
}
}Rebuild and test the enhanced functionality:
dotnet build
# Copy the updated DLL to the plugins directory
# Test with input file
echo "Alice" > name.txt
CoseSignTool hello --input name.txt
# Test with output file
CoseSignTool hello --name "Bob" --output greeting.txt
cat greeting.txtNow that you have a basic plugin working:
- Read the full Plugins.md documentation for comprehensive details
- Study the MST Plugin (
CoseSignTool.MST.Plugin) for a real-world example - Add error handling for edge cases and invalid inputs
- Implement validation for required parameters
- Add unit tests for your plugin commands
- Consider security implications of file operations and user input
// Required parameter
string endpoint = GetRequiredValue(configuration, "endpoint");
// Optional parameter with default
string timeout = GetOptionalValue(configuration, "timeout", "30") ?? "30";
// Optional parameter that may be null
string? metadata = GetOptionalValue(configuration, "metadata");string filePath = GetRequiredValue(configuration, "file");
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"File not found: {filePath}");
return PluginExitCode.UserSpecifiedFileNotFound;
}// Check before expensive operations
cancellationToken.ThrowIfCancellationRequested();
// Pass to async methods
await someAsyncMethod(cancellationToken);
// Use in loops
for (int i = 0; i < items.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessItem(items[i], cancellationToken);
}Plugin not found:
- Ensure assembly name ends with
.Plugin.dll - Verify the DLL is in the
pluginsdirectory - Check that your class implements
ICoseSignToolPlugin
Command not working:
- Verify the
Optionsdictionary maps command-line arguments correctly - Check for typos in option names
- Ensure proper error handling and return codes
Security errors:
- Make sure plugins are only in the
pluginssubdirectory - Don't try to load plugins from other locations
For more detailed information, see the complete Plugins.md documentation.