CoseSignTool supports a plugin architecture that allows developers to extend the tool's functionality with custom commands and integrations. This document provides a comprehensive guide for creating, deploying, and using plugins with CoseSignTool.
The plugin system enables:
- Custom Commands: Add new commands beyond the built-in
sign,validate, andgetcommands - Certificate Provider Plugins: Extend signing capabilities with custom certificate sources (cloud HSMs, remote signing services, hardware tokens)
- Third-party Integrations: Connect with external services, APIs, and workflows
- Extensible Architecture: Maintain separation between core functionality and specialized features
- Security: Plugins are only loaded from the secure
pluginssubdirectory
CoseSignTool supports two types of plugins:
Add new top-level commands to CoseSignTool (e.g., mst_register, indirect-sign). These plugins:
- Provide standalone commands with their own parameters and behavior
- Are executed as:
CoseSignTool <command-name> [options] - Integrate with the main command dispatcher
- Appear in the main help output under "Plugin Commands"
Extend the sign and indirect-sign commands with custom certificate sources. These plugins:
- Provide signing key providers for certificate-based operations
- Are used via:
CoseSignTool sign --cert-provider <provider-name> [provider-options] - Integrate directly with the signing workflow
- Appear in help output under "Certificate Providers"
- Enable signing with cloud HSMs, hardware tokens, remote signing services, etc.
📖 Detailed Documentation: For comprehensive certificate provider plugin documentation, see CertificateProviders.md.
The plugin system is built around several key interfaces defined in the CoseSignTool.Abstractions namespace:
The main plugin interface for command plugins:
public interface ICoseSignToolPlugin
{
string Name { get; } // Plugin display name
string Version { get; } // Plugin version (semver recommended)
string Description { get; } // Brief description for help output
IEnumerable<IPluginCommand> Commands { get; } // Commands provided by this plugin
void Initialize(IConfiguration? configuration = null); // Plugin initialization
}The interface for certificate provider plugins that extend signing capabilities:
public interface ICertificateProviderPlugin
{
string ProviderName { get; } // Unique provider identifier (e.g., "azure-artifact-signing")
string Description { get; } // Provider description for help output
IDictionary<string, string> GetProviderOptions(); // Provider-specific command-line options
bool CanCreateProvider(IConfiguration configuration); // Check if required parameters are present
ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null); // Create signing key provider
}Key Concepts:
- Provider Name: Lowercase, hyphenated identifier used with
--cert-providerparameter - Provider Options: Custom command-line parameters specific to the provider (e.g.,
--aas-endpoint) - Signing Key Provider: Returns an
ICoseSigningKeyProviderfor certificate-based signing operations - Configuration-Based: Uses
IConfigurationto access command-line parameters and settings
Integration with Sign Commands:
Certificate provider plugins integrate seamlessly with the built-in sign and plugin-based indirect-sign commands:
# Using with built-in sign command
CoseSignTool sign --payload file.txt --cert-provider azure-artifact-signing --aas-endpoint https://... --aas-account-name myaccount
# Using with indirect-sign plugin command
CoseSignTool indirect-sign --payload file.txt --signature file.cose --cert-provider azure-artifact-signing --aas-endpoint https://...Interface for individual commands within a plugin:
public interface IPluginCommand
{
string Name { get; } // Command name (e.g., "register", "verify")
string Description { get; } // Command description for help
string Usage { get; } // Usage instructions
IDictionary<string, string> Options { get; } // Command-line options mapping
Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default);
}Abstract base class providing common functionality for plugin commands:
public abstract class PluginCommandBase : IPluginCommand
{
// Abstract members to implement
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);
// Helper methods
protected static string GetRequiredValue(IConfiguration configuration, string key);
protected static string? GetOptionalValue(IConfiguration configuration, string key, string? defaultValue = null);
}Plugins use the PluginExitCode enum to indicate command execution results:
Success(0): Command completed successfullyHelpRequested(1): User requested help for the commandMissingRequiredOption(2): A required command-line option was missingUnknownArgument(3): An unrecognized command-line argument was providedInvalidArgumentValue(4): A command-line argument had an invalid valueMissingArgumentValue(5): A required argument value was missingUserSpecifiedFileNotFound(6): A user-specified file was not foundUnknownError(10): An unexpected error occurred
Create a new .NET 8.0 class library project:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>YourCompany.YourService.Plugin</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\CoseSignTool.Abstractions\CoseSignTool.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>- Runtime Discovery: The assembly name must end with
.Plugin.dllfor automatic discovery by CoseSignTool - CI/CD Auto-Packaging: The project file must end with
.Plugin.csprojfor automatic inclusion in CI/CD builds
The CoseSignTool CI/CD pipeline automatically discovers and packages any project following this naming pattern:
<ProjectName>.Plugin.csproj
CoseSignTool.MST.Plugin.csproj→ Automatically built and deployedCoseSignTool.IndirectSignature.Plugin.csproj→ Automatically built and deployedYourCompany.CustomSigning.Plugin.csproj→ Would be automatically built and deployedAzureKeyVault.Integration.Plugin.csproj→ Would be automatically built and deployed
CoseSignTool.Utilities.csproj→ Not a plugin (missing.Pluginsuffix)CustomSigningTool.csproj→ Not a plugin (missing.Pluginsuffix)MyPlugin.csproj→ Not a plugin (missing.Pluginsuffix)
When you follow the .Plugin.csproj naming convention:
✅ Automatic CI/CD Integration: No manual updates needed to build scripts
✅ Automatic Packaging: Plugin included in all releases automatically
✅ Automatic Discovery: Plugin commands appear in CoseSignTool help
✅ Automatic Testing: Plugin included in CI/CD test runs
The CI/CD pipeline uses this discovery command:
# Automatically finds all plugin projects
PLUGIN_PROJECTS=($(find . -name "*.Plugin.csproj" -type f))This means adding a new plugin requires no maintenance - just follow the naming convention!
using CoseSignTool.Abstractions;
using Microsoft.Extensions.Configuration;
namespace YourCompany.YourService.Plugin;
public class YourServicePlugin : ICoseSignToolPlugin
{
private readonly List<IPluginCommand> _commands;
public YourServicePlugin()
{
_commands = new List<IPluginCommand>
{
new RegisterCommand(),
new VerifyCommand(),
new StatusCommand()
};
}
public string Name => "Your Service Integration";
public string Version =>
System.Reflection.Assembly.GetExecutingAssembly()
.GetName()
.Version?
.ToString() ?? "1.0.0";
public string Description => "Provides integration with Your Service for COSE signatures.";
public IEnumerable<IPluginCommand> Commands => _commands;
public void Initialize(IConfiguration? configuration = null)
{
// Perform plugin initialization
// - Validate service connectivity
// - Load default configurations
// - Set up logging
}
}using CoseSignTool.Abstractions;
using Microsoft.Extensions.Configuration;
namespace YourCompany.YourService.Plugin;
public class RegisterCommand : PluginCommandBase
{
public override string Name => "your_register";
public override string Description => "Register a COSE signature with Your Service";
public override string Usage => @"
your_register - Register a COSE signature with Your Service
Usage:
CoseSignTool your_register --endpoint <url> --payload <file> --signature <file> [options]
Required Options:
--endpoint Your Service endpoint URL
--payload Path to the original payload file
--signature Path to the COSE signature file
Optional Options:
--timeout Request timeout in seconds (default: 30)
--credential Authentication credential type (default: default)
--output Output file for service response
--metadata Additional metadata as JSON string
";
public override IDictionary<string, string> Options => new Dictionary<string, string>
{
["--endpoint"] = "endpoint",
["--payload"] = "payload",
["--signature"] = "signature",
["--timeout"] = "timeout",
["--credential"] = "credential",
["--output"] = "output",
["--metadata"] = "metadata"
};
public override async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default)
{
try
{
// Check for cancellation
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(cancellationToken);
}
// Get required configuration values
string endpoint = GetRequiredValue(configuration, "endpoint");
string payloadPath = GetRequiredValue(configuration, "payload");
string signaturePath = GetRequiredValue(configuration, "signature");
// Get optional configuration values
string? timeoutStr = GetOptionalValue(configuration, "timeout", "30");
string credential = GetOptionalValue(configuration, "credential", "default") ?? "default";
string? outputPath = GetOptionalValue(configuration, "output");
string? metadata = GetOptionalValue(configuration, "metadata");
// Validate inputs
if (!File.Exists(payloadPath))
{
Console.Error.WriteLine($"Payload file not found: {payloadPath}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
if (!File.Exists(signaturePath))
{
Console.Error.WriteLine($"Signature file not found: {signaturePath}");
return PluginExitCode.UserSpecifiedFileNotFound;
}
if (!int.TryParse(timeoutStr, out int timeout) || timeout <= 0)
{
Console.Error.WriteLine($"Invalid timeout value: {timeoutStr}");
return PluginExitCode.InvalidArgumentValue;
}
// Check for cancellation before expensive operations
cancellationToken.ThrowIfCancellationRequested();
// Implement your service integration logic here
var result = await RegisterWithYourService(
endpoint, payloadPath, signaturePath,
timeout, credential, metadata, cancellationToken);
// Handle output
if (!string.IsNullOrEmpty(outputPath))
{
await File.WriteAllTextAsync(outputPath, result, cancellationToken);
Console.WriteLine($"Service response written to: {outputPath}");
}
else
{
Console.WriteLine("Registration successful");
Console.WriteLine(result);
}
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 (Exception ex)
{
Console.Error.WriteLine($"Error during registration: {ex.Message}");
return PluginExitCode.UnknownError;
}
}
private async Task<string> RegisterWithYourService(string endpoint, string payloadPath,
string signaturePath, int timeout, string credential, string? metadata,
CancellationToken cancellationToken)
{
// Implement your service-specific registration logic
// - Read payload and signature files
// - Make HTTP requests to your service
// - Handle authentication
// - Process service responses
// - Respect cancellation token
throw new NotImplementedException("Implement your service integration here");
}
}Certificate provider plugins extend the signing capabilities of CoseSignTool by integrating custom certificate sources such as cloud HSMs, hardware security tokens, remote signing services, or proprietary key management systems.
Certificate provider plugins are ideal for:
- Cloud HSM Integration: Azure Key Vault, AWS KMS, Google Cloud KMS
- Remote Signing Services: Azure Artifact Signing, DigiCert ONE, GlobalSign DSS
- Hardware Security Modules: Thales Luna, Utimaco, nCipher
- Smart Cards and Tokens: YubiKey, TPM, PIV cards
- Custom Key Management: Proprietary key storage and signing infrastructure
Create a .NET 8.0 class library with the certificate provider dependencies:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>YourCompany.YourCertProvider.Plugin</AssemblyName>
<!-- Required for plugin dependency isolation -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PreserveCompilationContext>true</PreserveCompilationContext>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\CoseSignTool.Abstractions\CoseSignTool.Abstractions.csproj" />
<ProjectReference Include="..\..\CoseSign1.Certificates\CoseSign1.Certificates.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Mark all external dependencies for copying -->
<PackageReference Include="YourHSM.Client" Version="1.0.0">
<Private>true</Private>
</PackageReference>
<PackageReference Include="YourAuth.Library" Version="2.0.0">
<Private>true</Private>
</PackageReference>
</ItemGroup>
</Project>using CoseSign1.Abstractions.Interfaces;
using CoseSignTool.Abstractions;
using Microsoft.Extensions.Configuration;
namespace YourCompany.YourCertProvider.Plugin;
/// <summary>
/// Certificate provider plugin for Your HSM/Service integration.
/// </summary>
public class YourCertProviderPlugin : ICertificateProviderPlugin
{
/// <summary>
/// Unique identifier for this provider (used with --cert-provider parameter).
/// Use lowercase with hyphens for multiple words.
/// </summary>
public string ProviderName => "your-cert-provider";
/// <summary>
/// Human-readable description shown in help output.
/// </summary>
public string Description => "Your HSM/Service certificate provider integration";
/// <summary>
/// Defines provider-specific command-line options.
/// These are merged into sign/indirect-sign commands when this provider is selected.
/// </summary>
public IDictionary<string, string> GetProviderOptions()
{
return new Dictionary<string, string>
{
// Use a provider-specific prefix to avoid conflicts
["--your-endpoint"] = "your-endpoint",
["--your-key-id"] = "your-key-id",
["--your-account"] = "your-account",
["--your-auth-method"] = "your-auth-method",
["-ye"] = "your-endpoint", // Short aliases
["-yk"] = "your-key-id"
};
}
/// <summary>
/// Checks if the configuration contains all required parameters.
/// Called before CreateProvider to validate inputs quickly.
/// </summary>
public bool CanCreateProvider(IConfiguration configuration)
{
// Check for required parameters
string? endpoint = configuration["your-endpoint"];
string? keyId = configuration["your-key-id"];
return !string.IsNullOrWhiteSpace(endpoint) &&
!string.IsNullOrWhiteSpace(keyId);
}
/// <summary>
/// Creates the signing key provider instance.
/// Called when the user specifies --cert-provider your-cert-provider.
/// </summary>
public ICoseSigningKeyProvider CreateProvider(
IConfiguration configuration,
IPluginLogger? logger = null)
{
// Extract configuration
string endpoint = configuration["your-endpoint"]
?? throw new ArgumentException("Missing required parameter: --your-endpoint");
string keyId = configuration["your-key-id"]
?? throw new ArgumentException("Missing required parameter: --your-key-id");
string? account = configuration["your-account"];
string authMethod = configuration["your-auth-method"] ?? "default";
logger?.LogVerbose($"Creating signing key provider for endpoint: {endpoint}");
logger?.LogVerbose($"Key ID: {keyId}");
logger?.LogVerbose($"Authentication method: {authMethod}");
// Create and return your signing key provider
return new YourCertProviderSigningKeyProvider(
endpoint, keyId, account, authMethod, logger);
}
}using CoseSign1.Abstractions.Interfaces;
using CoseSignTool.Abstractions;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace YourCompany.YourCertProvider.Plugin;
/// <summary>
/// Signing key provider that connects to your HSM/service.
/// </summary>
public class YourCertProviderSigningKeyProvider : ICoseSigningKeyProvider
{
private readonly string _endpoint;
private readonly string _keyId;
private readonly string? _account;
private readonly string _authMethod;
private readonly IPluginLogger? _logger;
private X509Certificate2? _certificate;
private AsymmetricAlgorithm? _privateKey;
public YourCertProviderSigningKeyProvider(
string endpoint,
string keyId,
string? account,
string authMethod,
IPluginLogger? logger)
{
_endpoint = endpoint;
_keyId = keyId;
_account = account;
_authMethod = authMethod;
_logger = logger;
}
/// <summary>
/// Gets the signing certificate (public key portion).
/// Called by CoseSignTool to obtain the certificate for the signature.
/// </summary>
public X509Certificate2 Certificate
{
get
{
if (_certificate == null)
{
_logger?.LogInformation("Retrieving certificate from HSM/service...");
_certificate = RetrieveCertificateFromService();
}
return _certificate;
}
}
/// <summary>
/// Gets the hash algorithm name supported by this provider.
/// </summary>
public HashAlgorithmName HashAlgorithm => HashAlgorithmName.SHA256;
/// <summary>
/// Gets the private key for signing operations.
/// This is typically a remote key that delegates signing to your HSM/service.
/// </summary>
public AsymmetricAlgorithm PrivateKey
{
get
{
if (_privateKey == null)
{
_logger?.LogVerbose("Creating remote signing key wrapper...");
_privateKey = CreateRemoteSigningKey();
}
return _privateKey;
}
}
/// <summary>
/// Gets additional certificates in the certificate chain (optional).
/// </summary>
public List<X509Certificate2>? AdditionalCertificates => null;
/// <summary>
/// Gets the issuer identifier for this signing key provider (optional).
/// For certificate-based providers, this can be a DID:X509 identifier.
/// </summary>
public string? Issuer => null;
/// <summary>
/// Retrieves the certificate from your HSM/service.
/// </summary>
private X509Certificate2 RetrieveCertificateFromService()
{
try
{
// TODO: Implement your service-specific logic to retrieve the certificate
// Example: Call your HSM API to get the certificate by key ID
// For demonstration:
// var client = new YourHSMClient(_endpoint, GetCredentials());
// var certBytes = await client.GetCertificateAsync(_keyId);
// return new X509Certificate2(certBytes);
throw new NotImplementedException("Implement certificate retrieval from your HSM/service");
}
catch (Exception ex)
{
_logger?.LogError($"Failed to retrieve certificate: {ex.Message}");
throw;
}
}
/// <summary>
/// Creates a signing key wrapper that delegates to your HSM/service.
/// </summary>
private AsymmetricAlgorithm CreateRemoteSigningKey()
{
// TODO: Create a custom AsymmetricAlgorithm implementation
// that delegates SignHash() calls to your HSM/service
// Example implementation structure:
// return new YourRemoteRSA(
// _endpoint,
// _keyId,
// GetCredentials(),
// _logger);
throw new NotImplementedException("Implement remote signing key wrapper");
}
/// <summary>
/// Gets credentials for authentication with your HSM/service.
/// SECURITY: Use secure credential mechanisms (DefaultAzureCredential, environment variables, etc.)
/// NEVER accept credentials directly on the command line.
/// </summary>
private object GetCredentials()
{
// TODO: Implement secure credential retrieval
// Examples:
// - Use DefaultAzureCredential for Azure services
// - Read from secure environment variables
// - Use certificate-based authentication
// - Use Windows Credential Manager
// - Use OAuth2 flows
switch (_authMethod.ToLowerInvariant())
{
case "azure":
// return new DefaultAzureCredential();
throw new NotImplementedException("Implement Azure authentication");
case "certificate":
// Load client certificate for mutual TLS
throw new NotImplementedException("Implement certificate authentication");
case "default":
default:
// Default authentication method
throw new NotImplementedException("Implement default authentication");
}
}
public void Dispose()
{
_certificate?.Dispose();
_privateKey?.Dispose();
}
}For HSM/remote signing scenarios, you need a custom AsymmetricAlgorithm implementation:
using System.Security.Cryptography;
namespace YourCompany.YourCertProvider.Plugin;
/// <summary>
/// RSA implementation that delegates signing operations to a remote HSM/service.
/// </summary>
internal class YourRemoteRSA : RSA
{
private readonly string _endpoint;
private readonly string _keyId;
private readonly object _credentials;
private readonly IPluginLogger? _logger;
private RSAParameters? _publicKeyParameters;
public YourRemoteRSA(
string endpoint,
string keyId,
object credentials,
IPluginLogger? logger)
{
_endpoint = endpoint;
_keyId = keyId;
_credentials = credentials;
_logger = logger;
}
/// <summary>
/// Signs data by calling the remote HSM/service.
/// This is the critical method called by the signing workflow.
/// </summary>
public override byte[] SignHash(
byte[] hash,
HashAlgorithmName hashAlgorithm,
RSASignaturePadding padding)
{
try
{
_logger?.LogVerbose($"Signing hash with remote key: {_keyId}");
_logger?.LogVerbose($"Hash algorithm: {hashAlgorithm.Name}, Padding: {padding}");
// TODO: Implement remote signing call to your HSM/service
// Example:
// var client = new YourHSMClient(_endpoint, _credentials);
// var signature = await client.SignHashAsync(_keyId, hash, hashAlgorithm, padding);
// return signature;
throw new NotImplementedException("Implement remote hash signing");
}
catch (Exception ex)
{
_logger?.LogError($"Remote signing failed: {ex.Message}");
throw;
}
}
/// <summary>
/// Exports the public key parameters.
/// Required for signature verification.
/// </summary>
public override RSAParameters ExportParameters(bool includePrivateParameters)
{
if (includePrivateParameters)
{
throw new CryptographicException("Cannot export private parameters from remote key");
}
if (_publicKeyParameters == null)
{
// TODO: Retrieve public key parameters from your HSM/service
throw new NotImplementedException("Implement public key parameter export");
}
return _publicKeyParameters.Value;
}
public override void ImportParameters(RSAParameters parameters)
{
throw new NotSupportedException("Cannot import parameters to a remote key");
}
// Implement other required RSA methods as needed...
}-
Never Accept Secrets on Command Line:
// ❌ BAD: Direct credential parameters ["--api-key"] = "api-key" // Don't do this! // ✅ GOOD: Secure credential mechanisms ["--credential-source"] = "credential-source" // Options: "azure", "env-var", "keychain"
-
Use Secure Credential Storage:
- Azure:
DefaultAzureCredential - AWS:
DefaultAWSCredentialsProviderChain - Environment variables for CI/CD
- Windows Credential Manager
- OS keychains (macOS Keychain, GNOME Keyring)
- Azure:
-
Validate All Inputs:
public bool CanCreateProvider(IConfiguration configuration) { string? endpoint = configuration["your-endpoint"]; // Validate endpoint format if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) || (uri.Scheme != "https" && uri.Scheme != "http")) { return false; } return true; }
-
Handle Sensitive Data Carefully:
// Clear sensitive data when no longer needed public void Dispose() { _certificate?.Dispose(); _privateKey?.Dispose(); // Clear any cached credentials }
-
Use Timeouts and Retries:
private async Task<byte[]> SignHashWithRetry(byte[] hash) { int maxRetries = 3; for (int i = 0; i < maxRetries; i++) { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); return await SignHashAsync(hash, cts.Token); } catch (TimeoutException) when (i < maxRetries - 1) { _logger?.LogWarning($"Signing timeout, retry {i + 1}/{maxRetries}"); await Task.Delay(1000 * (i + 1)); // Exponential backoff } } throw new Exception("Signing failed after retries"); }
[TestClass]
public class YourCertProviderPluginTests
{
[TestMethod]
public void ProviderName_ShouldBeKebabCase()
{
var plugin = new YourCertProviderPlugin();
Assert.AreEqual("your-cert-provider", plugin.ProviderName);
}
[TestMethod]
public void GetProviderOptions_ShouldReturnRequiredOptions()
{
var plugin = new YourCertProviderPlugin();
var options = plugin.GetProviderOptions();
Assert.IsTrue(options.ContainsKey("--your-endpoint"));
Assert.IsTrue(options.ContainsKey("--your-key-id"));
}
[TestMethod]
public void CanCreateProvider_WithMissingEndpoint_ShouldReturnFalse()
{
var plugin = new YourCertProviderPlugin();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["your-key-id"] = "test-key"
// Missing endpoint
})
.Build();
Assert.IsFalse(plugin.CanCreateProvider(config));
}
[TestMethod]
public void CanCreateProvider_WithAllRequired_ShouldReturnTrue()
{
var plugin = new YourCertProviderPlugin();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["your-endpoint"] = "https://test.example.com",
["your-key-id"] = "test-key"
})
.Build();
Assert.IsTrue(plugin.CanCreateProvider(config));
}
}For a complete, production-ready example of a certificate provider plugin, see:
- Source Code:
CoseSignTool.AzureArtifactSigning.Plugin/ - Documentation: CertificateProviders.md
- Usage Guide: CoseSign1.Certificates.AzureArtifactSigning.md
The Azure Artifact Signing plugin demonstrates:
- Integration with Azure cloud-based signing service
- DefaultAzureCredential authentication
- Comprehensive error handling
- Provider-specific parameters (
--aas-endpoint,--aas-account-name,--aas-cert-profile-name) - Full test coverage
For security reasons, CoseSignTool only loads plugins from the plugins subdirectory of the executable. Since version 2.0, CoseSignTool supports both legacy flat and enhanced subdirectory-based plugin architectures:
Enhanced Subdirectory Architecture (Recommended):
CoseSignTool.exe
└── plugins/
├── YourCompany.YourService.Plugin/
│ ├── YourCompany.YourService.Plugin.dll
│ ├── YourSpecificDependency.dll
│ ├── AnotherDependency.dll
│ └── ...
├── AnotherCompany.AnotherService.Plugin/
│ ├── AnotherCompany.AnotherService.Plugin.dll
│ ├── SpecificDependencyV1.dll
│ └── ...
└── [legacy flat files for backward compatibility]
Legacy Flat Architecture (Supported):
CoseSignTool.exe
└── plugins/
├── YourCompany.YourService.Plugin.dll
├── AnotherCompany.AnotherService.Plugin.dll
├── SharedDependency.dll
└── ...
Key Benefits of Subdirectory Architecture:
- Dependency Isolation: Each plugin has its own dependency context, preventing version conflicts
- Self-Contained Deployment: All plugin dependencies are contained within the plugin's subdirectory
- Easier Distribution: Plugins can be packaged as complete, self-contained units
- Better Maintainability: Clear separation between different plugins and their dependencies
- Concurrent Versions: Multiple plugins can use different versions of the same dependency
Security Features:
- Path validation: The
PluginLoader.ValidatePluginDirectory()method ensures plugins are only loaded from the authorized directory - Path normalization: Handles different path formats and prevents directory traversal attacks
- Exception throwing: Attempts to load plugins from unauthorized locations throw
UnauthorizedAccessException
The plugin discovery process supports both legacy flat and modern subdirectory structures:
Enhanced Discovery Process (Version 2.0+):
- Directory existence: Check if the
pluginsdirectory exists - Security validation: Verify the directory is authorized for plugin loading
- Subdirectory scanning:
- Search subdirectories for
*.Plugin.dllfiles - Create isolated AssemblyLoadContext for each plugin
- Load plugin dependencies from plugin-specific subdirectory
- Search subdirectories for
- Legacy fallback:
- Search for
*.Plugin.dllfiles in the main plugins directory - Load using default AssemblyLoadContext for backward compatibility
- Search for
- Type discovery: Find types implementing
ICoseSignToolPlugin - Instance creation: Create plugin instances using
Activator.CreateInstance()
Plugin Load Context:
- Each plugin in a subdirectory gets its own
PluginLoadContext(derived fromAssemblyLoadContext) - Dependencies are resolved first from the plugin's subdirectory
- Shared framework assemblies (System., Microsoft.Extensions.) are resolved from the main application context
- This prevents dependency conflicts between plugins while maintaining shared framework compatibility
The plugin system includes comprehensive error handling:
- Assembly loading errors: Handled gracefully with warning messages
- Type loading errors: Reported without stopping other plugin loading
- Plugin initialization errors: Logged but don't prevent tool startup
- Command conflicts: Warn about duplicate command names
Enhanced Subdirectory Deployment (Recommended):
-
Build your plugin project with dependency copying enabled:
<PropertyGroup> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <PreserveCompilationContext>true</PreserveCompilationContext> <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> </PropertyGroup> <ItemGroup> <PackageReference Include="YourDependency" Version="1.0.0"> <Private>true</Private> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </PackageReference> </ItemGroup>
-
Create a subdirectory named after your plugin:
mkdir plugins/YourCompany.YourService.Plugin
-
Copy your plugin assembly and all its dependencies to the subdirectory:
# Copy main plugin assembly cp bin/Debug/net8.0/YourCompany.YourService.Plugin.dll plugins/YourCompany.YourService.Plugin/ # Copy all dependencies cp bin/Debug/net8.0/*.dll plugins/YourCompany.YourService.Plugin/
Legacy Flat Deployment (Backward Compatibility):
- Build your plugin project
- Copy the resulting
.dllfile to thepluginsdirectory next toCoseSignTool.exe - Include any required dependencies (but be careful about conflicts with CoseSignTool dependencies)
For plugins following the *.Plugin.csproj naming convention, deployment is fully automatic. Simply name your project correctly and place it under the CoseSignTool solution directory:
YourCompany.YourService.Plugin.csproj → Automatically discovered and deployed
The build system will:
- Discover your plugin during
dotnet build CoseSignTool - Build it with the same Configuration and RuntimeIdentifier
- Deploy it to
plugins/YourCompany.YourService.Plugin/subdirectory - Copy all plugin-specific dependencies
No manual MSBuild target configuration is required when following the naming convention.
For custom deployment scenarios (non-standard naming or special requirements), you can add a custom target:
<Target Name="DeployYourPlugin" AfterTargets="Build" Condition="'$(DeployPlugins)' == 'true'">
<PropertyGroup>
<PluginsDir>$(OutputPath)plugins</PluginsDir>
<YourPluginSubDir>$(PluginsDir)\YourCompany.YourService.Plugin</YourPluginSubDir>
<YourPluginDir>$(MSBuildProjectDirectory)\..\YourCompany.YourService.Plugin\bin\$(Configuration)\net8.0</YourPluginDir>
</PropertyGroup>
<MakeDir Directories="$(YourPluginSubDir)" />
<ItemGroup>
<YourPluginFiles Include="$(YourPluginDir)\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(YourPluginFiles)" DestinationFolder="$(YourPluginSubDir)" />
<Message Text="Your Plugin deployed to: $(YourPluginSubDir)" Importance="high" />
</Target>For distributing plugins, you now have improved options:
-
Self-Contained ZIP Archive: Package the plugin subdirectory with all dependencies
YourPlugin.zip └── YourCompany.YourService.Plugin/ ├── YourCompany.YourService.Plugin.dll ├── dependency1.dll ├── dependency2.dll └── ... -
NuGet Package: Create a package that includes the subdirectory structure
-
Installer: Create an installer that creates the subdirectory and places all files correctly
-
Container/Docker: Include plugins in container images with proper directory structure
Included with CoseSignTool:
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration.Abstractions
- System.Text.Json
- .NET 8.0 Base Class Library
Plugin-specific dependencies:
Enhanced Subdirectory Architecture:
- Complete Isolation: Package all dependencies with your plugin in its subdirectory
- Version Freedom: Use any version of dependencies without conflicts
- Self-Contained: Plugin works independently of other plugins' dependencies
- Shared Framework: Common .NET and Microsoft.Extensions assemblies are still shared for efficiency
Legacy Flat Architecture:
- Package them with your plugin in the main plugins directory
- Ensure version compatibility with CoseSignTool and other plugins
- Document any external dependencies
- Be careful about dependency conflicts
Recommended Dependency Management:
<!-- In your plugin .csproj file -->
<ItemGroup>
<!-- External dependencies with explicit copying -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3">
<Private>true</Private>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</PackageReference>
<!-- Azure dependencies isolated in plugin subdirectory -->
<PackageReference Include="Azure.Core" Version="1.46.1">
<Private>true</Private>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</PackageReference>
</ItemGroup>When CoseSignTool starts, it automatically:
- Scans the
pluginsdirectory for plugin assemblies - Loads and initializes discovered plugins
- Registers plugin commands with the main command dispatcher
- Includes plugin commands in help output
Plugin commands are executed just like built-in commands:
# Built-in command
CoseSignTool sign --payload myfile.txt --certificate mycert.pfx
# Plugin command
CoseSignTool your_register --endpoint https://yourservice.com --payload myfile.txt --signature myfile.txt.cosePlugins are automatically included in the help system:
Command Plugins:
# General help shows all plugin commands under "Plugin Commands"
CoseSignTool --help
# Output:
# Plugin Commands:
# mst_register Register a COSE Sign1 message with Microsoft's Signing Transparency (MST)
# indirect-sign Creates an indirect COSE Sign1 signature for a payload file
# Plugin command help
CoseSignTool your_register --helpCertificate Provider Plugins:
# General help shows all certificate providers under "Certificate Providers"
CoseSignTool --help
# Output:
# Certificate Providers:
# azure-artifact-signing Azure Artifact Signing cloud-based certificate provider
# Sign command help shows certificate providers
CoseSignTool sign --help
# Output includes:
# Certificate Providers:
# The following certificate provider plugins are available for signing:
# azure-artifact-signing Azure Artifact Signing cloud-based certificate provider
# Usage: CoseSignTool sign <payload> --cert-provider azure-artifact-signing [options]
# Options:
# --aas-endpoint
# --aas-account-name
# --aas-cert-profile-name
# Indirect-sign command help also shows certificate providers
CoseSignTool indirect-sign --helpPlugin commands receive configuration through the standard .NET IConfiguration interface:
- Command-line arguments: Parsed and mapped according to the plugin's
Optionsdictionary - Environment variables: Available through the configuration system
- Configuration files: Can be loaded by plugins during initialization
The CoseSignTool includes a reference implementation for Azure Code Transparency Service integration.
📖 Complete Documentation: For comprehensive MST plugin documentation, including detailed authentication options, CI/CD integration examples, and troubleshooting, see MST.md.
The Microsoft's Signing Transparency (MST) plugin provides two main commands:
mst_register- Register signatures with Microsoft's Signing Transparency (MST)mst_verify- Verify signatures against Microsoft's Signing Transparency (MST)
CoseSignTool.MST.Plugin/
├── MstPlugin.cs # Main plugin class
├── RegisterCommand.cs # Command to register signatures
├── VerifyCommand.cs # Command to verify signatures
└── CoseSignTool.MST.Plugin.csproj
# Register a signature with Microsoft's Signing Transparency (MST) using default environment variable
export MST_TOKEN="your-access-token"
CoseSignTool mst_register \
--endpoint https://your-cts-instance.azure.com \
--payload myfile.txt \
--signature myfile.txt.cose
# Register a signature with Microsoft's Signing Transparency (MST) using custom environment variable
export MY_MST_TOKEN="your-access-token"
CoseSignTool mst_register \
--endpoint https://your-cts-instance.azure.com \
--payload myfile.txt \
--signature myfile.txt.cose \
--token-env-var MY_MST_TOKEN
# Verify a signature with Microsoft's Signing Transparency (MST)
export MST_TOKEN="your-access-token"
CoseSignTool mst_verify \
--endpoint https://your-cts-instance.azure.com \
--payload myfile.txt \
--signature myfile.txt.cose \
--receipt receipt.json
# Using Azure DefaultCredential when no token is provided
CoseSignTool mst_register \
--endpoint https://your-cts-instance.azure.com \
--payload myfile.txt \
--signature myfile.txt.coseThe MST plugin supports multiple authentication methods with the following priority:
-
Environment Variable Token: Uses an access token from an environment variable
--token-env-varspecifies the environment variable name- If not specified, defaults to
MST_TOKEN - This is the recommended approach for CI/CD environments
-
Azure DefaultCredential: Falls back to Azure DefaultCredential when no token is found
- Automatically uses available Azure credentials (managed identity, Azure CLI, etc.)
- Ideal for local development and Azure-hosted environments
# Using default environment variable
export MST_TOKEN="your-access-token"
CoseSignTool mst_register --endpoint https://your-cts-instance.azure.com --payload file.txt --signature file.cose
# Using custom environment variable
export MY_CUSTOM_TOKEN="your-access-token"
CoseSignTool mst_register --endpoint https://your-mst-instance.azure.com --payload file.txt --signature file.cose --token-env-var MY_CUSTOM_TOKEN
# Using Azure DefaultCredential (no token environment variable set)
# Requires Azure CLI login, managed identity, or other Azure credential
CoseSignTool mst_register --endpoint https://your-cts-instance.azure.com --payload file.txt --signature file.coseThe CoseSignTool includes a production-ready certificate provider plugin for Azure Artifact Signing, demonstrating best practices for cloud-based signing services.
📖 Complete Documentation: For comprehensive Azure Artifact Signing documentation, including setup, authentication, and advanced scenarios, see CertificateProviders.md and CoseSign1.Certificates.AzureArtifactSigning.md.
The Azure Artifact Signing plugin integrates with the sign and indirect-sign commands:
# Sign with Azure Artifact Signing (using DefaultAzureCredential)
CoseSignTool sign --payload myfile.txt \
--cert-provider azure-artifact-signing \
--aas-endpoint https://myaccount.codesigning.azure.net \
--aas-account-name myaccount \
--aas-cert-profile-name myprofile
# Indirect sign with Azure Artifact Signing
CoseSignTool indirect-sign --payload myfile.txt --signature myfile.cose \
--cert-provider azure-artifact-signing \
--aas-endpoint https://myaccount.codesigning.azure.net \
--aas-account-name myaccount \
--aas-cert-profile-name myprofileCoseSignTool.AzureArtifactSigning.Plugin/
├── AzureArtifactSigningPlugin.cs # Main plugin implementation (ICertificateProviderPlugin)
├── CoseSignTool.AzureArtifactSigning.Plugin.csproj # Project with dependency isolation
└── [Dependencies copied to plugins/CoseSignTool.AzureArtifactSigning.Plugin/]
├── Azure.CodeSigning.dll
├── Azure.Core.dll
├── Azure.Developer.ArtifactSigning.CryptoProvider.dll
└── ...
- Dependency Isolation: All Azure dependencies packaged in plugin subdirectory
- Secure Authentication: DefaultAzureCredential for passwordless auth
- Provider-Specific Options: Custom parameters (
--ats-*) with prefix to avoid conflicts - Integration with CoseSign1.Certificates: Uses
AzureArtifactSigningCoseSigningKeyProvider - Comprehensive Testing: Full unit test coverage demonstrating plugin testing patterns
// Azure Artifact Signing uses DefaultAzureCredential:
// 1. Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET)
// 2. Managed Identity (when running in Azure)
// 3. Visual Studio / VS Code credentials
// 4. Azure CLI credentials (az login)
// 5. Azure PowerShell credentials
// Never requires credentials on command line
CoseSignTool sign --payload file.txt \
--cert-provider azure-artifact-signing \
--aas-endpoint https://myaccount.codesigning.azure.net \
--aas-account-name myaccount \
--aas-cert-profile-name myprofileGitHub Actions:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Sign with Azure Artifact Signing
run: |
CoseSignTool sign --payload artifact.bin \
--cert-provider azure-artifact-signing \
--aas-endpoint ${{ secrets.AAS_ENDPOINT }} \
--aas-account-name ${{ secrets.AAS_ACCOUNT }} \
--aas-cert-profile-name ${{ secrets.AAS_PROFILE }}Azure DevOps:
- task: AzureCLI@2
displayName: 'Sign with Azure Artifact Signing'
inputs:
azureSubscription: 'MyServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
CoseSignTool sign --payload artifact.bin \
--cert-provider azure-artifact-signing \
--aas-endpoint $(AAS_ENDPOINT) \
--aas-account-name $(AAS_ACCOUNT) \
--aas-cert-profile-name $(AAS_PROFILE)- Error Handling: Use appropriate
PluginExitCodevalues for different error scenarios - Cancellation Support: Always respect the
CancellationTokenin async operations - Input Validation: Validate all user inputs and provide clear error messages
- Resource Management: Properly dispose of resources and handle cleanup
- Security: Never trust user input; validate and sanitize all data
- Logging: Use Console.Error for error messages and Console.Out for regular output
- Secure Credentials: Use
DefaultAzureCredential, environment variables, or OS credential stores - never command-line parameters - Provider Naming: Use lowercase with hyphens (e.g.,
azure-artifact-signing, notAzureArtifactSigning) - Option Prefixing: Prefix provider-specific options to avoid conflicts (e.g.,
--aas-endpoint, not--endpoint) - Validation: Implement
CanCreateProviderto quickly validate required parameters - Remote Signing: Implement custom
AsymmetricAlgorithmsubclass for HSM/remote signing - Certificate Chains: Include intermediate certificates via
AdditionalCertificatesproperty - Timeout Handling: Implement timeouts and retries for network operations
- Logging: Use
IPluginLoggerfor diagnostic output (Verbose, Information, Warning, Error)
- Naming: Use descriptive, consistent command names (e.g.,
service_action) - Options: Provide both long names and short aliases for common options
- Help: Include comprehensive usage information and examples
- Backwards Compatibility: Maintain API compatibility across plugin versions
- Unit Tests: Test individual command logic thoroughly
- Integration Tests: Test plugin loading and command execution
- Error Cases: Test all error scenarios and exit codes
- Cancellation: Test cancellation token handling
- Security: Test path validation and input sanitization
- Async Operations: Use async/await for I/O operations
- Resource Usage: Minimize memory usage for large files
- Timeouts: Implement reasonable timeouts for external operations
- Caching: Cache expensive operations when appropriate
Plugin not discovered:
- Verify the assembly name ends with
.Plugin.dll - Check that the file is in the
pluginsdirectory - Ensure the assembly implements
ICoseSignToolPlugin
Security errors:
- Verify plugins are in the correct
pluginssubdirectory - Check that the path doesn't contain traversal attempts (e.g.,
../)
Command conflicts:
- Check for duplicate command names across plugins
- Review console warnings during startup
Runtime errors:
- Check plugin dependencies are available
- Verify .NET 8.0 compatibility
- Review error messages in console output
- Console Output: Check startup warnings and error messages
- Plugin Loading: Verify plugin discovery and initialization
- Command Execution: Test commands with various inputs and edge cases
- Configuration: Ensure command-line options are properly mapped
If you have custom extensions built into CoseSignTool, you can migrate them to plugins:
- Extract Code: Move your extension code to a separate project
- Implement Interfaces: Implement the plugin interfaces
- Update Dependencies: Reference
CoseSignTool.Abstractions - Test Integration: Verify the plugin loads and functions correctly
Plugins should target the same .NET version as CoseSignTool and use compatible versions of shared dependencies. The plugin system is designed to be forward-compatible within major versions.
When contributing plugins or improvements to the plugin system:
- Follow Conventions: Use established patterns and naming conventions
- Add Tests: Include comprehensive tests for new functionality
- Update Documentation: Keep this documentation current with changes
- Security Review: Consider security implications of changes
- Backwards Compatibility: Avoid breaking changes to the plugin API
For more information, see the main CONTRIBUTING.md guide.