CoseSignTool supports an extensible Certificate Provider Plugin Architecture that allows you to use different signing key sources beyond local PFX files and certificate stores. This enables integration with cloud-based signing services, hardware security modules (HSMs), and other certificate providers.
- Overview
- Built-in Providers
- Using Certificate Providers
- Azure Artifact Signing
- Creating Custom Certificate Providers
- Security Best Practices
Certificate provider plugins extend CoseSignTool's signing capabilities by implementing the ICertificateProviderPlugin interface. Each plugin provides:
- A unique provider name
- Configuration parameters
- A factory method to create signing key providers
The plugin architecture automatically:
- Discovers plugins from
*.Plugin.dllassemblies - Merges provider-specific options with command options
- Validates configuration before creating providers
- Provides consistent error handling and logging
When no --cp is specified, CoseSignTool uses local certificate loading:
- PFX files: Load certificates with private keys from
.pfxfiles - Certificate stores: Access certificates from Windows/macOS/Linux certificate stores
Microsoft's cloud-based signing service providing:
- Managed certificates: Microsoft-managed certificate lifecycle
- Compliance: FIPS 140-2 Level 3 HSM-backed signing
- Integration: Seamless Azure DevOps and GitHub Actions integration
See Azure Artifact Signing section for details.
CoseSignTool sign --p <file> --cp <provider-name> [provider-options]CoseSignTool sign --help
# Shows all available certificate providers and their parametersCoseSignTool sign \
--p payload.txt \
--sf signature.cose \
--cp azure-artifact-signing \
--aas-endpoint https://contoso.codesigning.azure.net \
--aas-account-name ContosoAccount \
--aas-cert-profile-name ContosoProfileAzure Artifact Signing is Microsoft's cloud-based code signing service that provides secure, compliant signing without managing certificates locally.
- Azure Subscription: Active Azure subscription with billing enabled
- Azure Artifact Signing Account: Created in Azure Portal
- Certificate Profile: Configured with appropriate certificate type
- Permissions: Your Azure identity must have:
Code Signing Certificate Profile Signerrole on the certificate profile- Access to the Azure Artifact Signing account
Azure Artifact Signing uses Azure DefaultAzureCredential for authentication, which automatically tries authentication methods in this order:
-
Environment Variables (recommended for CI/CD)
export AZURE_TENANT_ID="your-tenant-id" export AZURE_CLIENT_ID="your-client-id" export AZURE_CLIENT_SECRET="your-client-secret"
-
Managed Identity (recommended for Azure VMs/containers)
- System-assigned or user-assigned managed identity
- Automatically available in Azure environments
-
Azure CLI (recommended for local development)
az login az account show # Verify correct subscription -
Azure PowerShell
Connect-AzAccount -
Visual Studio / Visual Studio Code
- Sign in to Azure through IDE
Security Note: DefaultAzureCredential excludes interactive browser authentication by default to prevent accidental prompts in unattended scenarios.
# Using Azure CLI authentication (local development)
az login
CoseSignTool sign \
--p document.pdf \
--sf document.pdf.cose \
--cp azure-artifact-signing \
--aas-endpoint https://contoso.codesigning.azure.net \
--aas-account-name ContosoAccount \
--aas-cert-profile-name ContosoProfilename: Sign Release
on:
release:
types: [created]
jobs:
sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sign artifacts
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
CoseSignTool sign \
--p release-artifact.bin \
--sf release-artifact.bin.cose \
--cp azure-artifact-signing \
--aas-endpoint ${{ secrets.AAS_ENDPOINT }} \
--aas-account-name ${{ secrets.AAS_ACCOUNT_NAME }} \
--aas-cert-profile-name ${{ secrets.AAS_CERT_PROFILE_NAME }}trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'MyServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
CoseSignTool sign \
--p $(Build.ArtifactStagingDirectory)/artifact.bin \
--sf $(Build.ArtifactStagingDirectory)/artifact.bin.cose \
--cp azure-artifact-signing \
--aas-endpoint $(AAS_ENDPOINT) \
--aas-account-name $(AAS_ACCOUNT_NAME) \
--aas-cert-profile-name $(AAS_CERT_PROFILE_NAME)CoseSignTool sign \
--p payload.txt \
--sf payload.cose \
--ep \
--cp azure-artifact-signing \
--aas-endpoint https://contoso.codesigning.azure.net \
--aas-account-name ContosoAccount \
--aas-cert-profile-name ContosoProfile \
--cwt-sub "software.release.v2.0" \
--cwt-aud "production.systems" \
--cwt "exp:2025-12-31T23:59:59Z"# Set Azure Artifact Signing configuration
export AAS_ENDPOINT="https://contoso.codesigning.azure.net"
export AAS_ACCOUNT_NAME="ContosoAccount"
export AAS_CERT_PROFILE_NAME="ContosoProfile"
# Azure authentication (service principal)
export AZURE_TENANT_ID="00000000-0000-0000-0000-000000000000"
export AZURE_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export AZURE_CLIENT_SECRET="your-client-secret"
# Sign multiple files
for file in *.bin; do
CoseSignTool sign \
--p "$file" \
--sf "${file}.cose" \
--cp azure-artifact-signing \
--aas-endpoint "$AAS_ENDPOINT" \
--aas-account-name "$AAS_ACCOUNT_NAME" \
--aas-cert-profile-name "$AAS_CERT_PROFILE_NAME"
done| Parameter | Alias | Required | Description |
|---|---|---|---|
--aas-endpoint |
Yes | Azure Artifact Signing endpoint URL (e.g., https://contoso.codesigning.azure.net) |
|
--aas-account-name |
Yes | Azure Artifact Signing account name | |
--aas-cert-profile-name |
Yes | Certificate profile name within the account |
Error: Azure.Identity.AuthenticationFailedException
Solution: Verify authentication method is configured correctly
# Check Azure CLI authentication
az account show
# Check environment variables
echo $AZURE_TENANT_ID
echo $AZURE_CLIENT_ID
# Test service principal authentication
az login --service-principal \
--username $AZURE_CLIENT_ID \
--password $AZURE_CLIENT_SECRET \
--tenant $AZURE_TENANT_IDError: Authorization failed. User does not have permission.
Solution: Verify RBAC role assignment
# Check role assignments
az role assignment list \
--assignee $AZURE_CLIENT_ID \
--query "[?roleDefinitionName=='Code Signing Certificate Profile Signer']"Error: Certificate provider 'azure-artifact-signing' cannot create a provider with the given configuration.
Solution: Verify all required parameters are provided
CoseSignTool sign \
--cp azure-artifact-signing \
--aas-endpoint "https://your-endpoint.codesigning.azure.net" \
--aas-account-name "YourAccount" \
--aas-cert-profile-name "YourProfile" \
--p test.txtAll certificate provider plugins must implement ICertificateProviderPlugin:
public interface ICertificateProviderPlugin
{
/// <summary>
/// Gets the unique name of this certificate provider (e.g., "azure-artifact-signing").
/// Used with the --cp command line parameter.
/// </summary>
string ProviderName { get; }
/// <summary>
/// Gets the available command-line options for this provider.
/// Keys are option names (e.g., "--aas-endpoint"), values are descriptions.
/// </summary>
IReadOnlyDictionary<string, string> GetProviderOptions();
/// <summary>
/// Determines if this provider can create a signing key provider with the given configuration.
/// Used for validation before attempting to create the provider.
/// </summary>
/// <param name="configuration">Configuration containing command-line parameters.</param>
/// <returns>True if all required parameters are present, false otherwise.</returns>
bool CanCreateProvider(IConfiguration configuration);
/// <summary>
/// Creates a signing key provider instance using the provided configuration.
/// </summary>
/// <param name="configuration">Configuration containing command-line parameters.</param>
/// <param name="logger">Optional logger for diagnostic messages.</param>
/// <returns>An ICoseSigningKeyProvider instance ready for signing operations.</returns>
ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null);
}Here's a complete example of a custom HSM certificate provider plugin:
using CoseSign1.Abstractions.Interfaces;
using CoseSignTool.Abstractions.Interfaces;
using Microsoft.Extensions.Configuration;
using System.Security.Cryptography.X509Certificates;
namespace MyCompany.Hsm.Plugin
{
/// <summary>
/// Certificate provider plugin for hardware security modules (HSMs).
/// </summary>
public class HsmCertificateProviderPlugin : ICertificateProviderPlugin
{
/// <inheritdoc/>
public string ProviderName => "hsm";
/// <inheritdoc/>
public IReadOnlyDictionary<string, string> GetProviderOptions()
{
return new Dictionary<string, string>
{
["--hsm-slot"] = "HSM slot number (required)",
["--hsm-pin"] = "HSM PIN for authentication (required)",
["--hsm-key-label"] = "Key label within the HSM (required)",
["--hsm-library-path"] = "Path to PKCS#11 library (optional, uses system default if not specified)"
};
}
/// <inheritdoc/>
public bool CanCreateProvider(IConfiguration configuration)
{
// Validate required parameters
return !string.IsNullOrWhiteSpace(configuration["hsm-slot"]) &&
!string.IsNullOrWhiteSpace(configuration["hsm-pin"]) &&
!string.IsNullOrWhiteSpace(configuration["hsm-key-label"]);
}
/// <inheritdoc/>
public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null)
{
logger?.LogInformation("Creating HSM certificate provider");
// Extract configuration
string slot = configuration["hsm-slot"]!;
string pin = configuration["hsm-pin"]!;
string keyLabel = configuration["hsm-key-label"]!;
string? libraryPath = configuration["hsm-library-path"];
// Create and return the provider
return new HsmCoseSigningKeyProvider(slot, pin, keyLabel, libraryPath, logger);
}
}
/// <summary>
/// Signing key provider implementation for HSM.
/// </summary>
internal class HsmCoseSigningKeyProvider : ICoseSigningKeyProvider
{
private readonly string _slot;
private readonly string _pin;
private readonly string _keyLabel;
private readonly string? _libraryPath;
private readonly IPluginLogger? _logger;
public HsmCoseSigningKeyProvider(
string slot,
string pin,
string keyLabel,
string? libraryPath,
IPluginLogger? logger)
{
_slot = slot;
_pin = pin;
_keyLabel = keyLabel;
_libraryPath = libraryPath;
_logger = logger;
}
public string? Issuer => null;
public X509Certificate2? GetSigningCertificate()
{
_logger?.LogInformation($"Retrieving certificate for key '{_keyLabel}' from HSM slot {_slot}");
// Initialize PKCS#11 library and retrieve certificate
// Implementation depends on your HSM provider's library
var pkcs11 = InitializePkcs11(_libraryPath);
var session = pkcs11.OpenSession(_slot, _pin);
var certificate = session.GetCertificate(_keyLabel);
return certificate;
}
public List<X509Certificate2>? GetCertificateChain()
{
_logger?.LogInformation("Retrieving certificate chain from HSM");
// Retrieve additional certificates from HSM if available
var pkcs11 = InitializePkcs11(_libraryPath);
var session = pkcs11.OpenSession(_slot, _pin);
var chain = session.GetCertificateChain(_keyLabel);
return chain;
}
public string? Issuer => null;
public RSA? ProvideRSAKey(PublicKey? publicKey = null)
{
_logger?.LogInformation($"Providing RSA key for '{_keyLabel}' from HSM");
// Create RSA wrapper that delegates to HSM
var pkcs11 = InitializePkcs11(_libraryPath);
var session = pkcs11.OpenSession(_slot, _pin);
var rsaKey = session.GetRsaKey(_keyLabel);
return rsaKey;
}
private IPkcs11Library InitializePkcs11(string? libraryPath)
{
// Initialize PKCS#11 library
// This is a placeholder - actual implementation depends on your HSM
throw new NotImplementedException("Initialize your PKCS#11 library here");
}
}
}Create a plugin project following the naming convention for automatic CI/CD packaging:
<!-- MyCompany.Hsm.Plugin.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyCompany.Hsm.Plugin</AssemblyName>
<!-- Important: Assembly name must end with .Plugin.dll -->
</PropertyGroup>
<ItemGroup>
<!-- Reference CoseSignTool.Abstractions for plugin interfaces -->
<ProjectReference Include="..\CoseSignTool.Abstractions\CoseSignTool.Abstractions.csproj" />
<!-- Add your HSM library dependencies -->
<PackageReference Include="Net.Pkcs11Interop" Version="5.1.2" />
</ItemGroup>
</Project>- Build your plugin project
- Copy the compiled
*.Plugin.dlland dependencies to thepluginsdirectory next toCoseSignTool.exe - Run CoseSignTool - it will automatically discover your plugin
# Build plugin
dotnet build MyCompany.Hsm.Plugin/MyCompany.Hsm.Plugin.csproj -c Release
# Deploy to CoseSignTool plugins directory
cp MyCompany.Hsm.Plugin/bin/Release/net8.0/*.dll /path/to/CoseSignTool/plugins/Follow the naming convention in PluginNamingConventions.md:
- Project file:
MyCompany.Hsm.Plugin.csproj - Assembly name:
MyCompany.Hsm.Plugin.dll
GitHub Actions will automatically include your plugin in releases!
Create unit tests for your plugin:
[TestFixture]
public class HsmCertificateProviderPluginTests
{
[Test]
public void ProviderName_ReturnsExpectedValue()
{
var plugin = new HsmCertificateProviderPlugin();
Assert.That(plugin.ProviderName, Is.EqualTo("hsm"));
}
[Test]
public void CanCreateProvider_WithAllRequiredParameters_ReturnsTrue()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["hsm-slot"] = "0",
["hsm-pin"] = "1234",
["hsm-key-label"] = "signing-key"
})
.Build();
var plugin = new HsmCertificateProviderPlugin();
Assert.That(plugin.CanCreateProvider(configuration), Is.True);
}
[Test]
public void CanCreateProvider_MissingRequiredParameter_ReturnsFalse()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["hsm-slot"] = "0"
// Missing pin and key-label
})
.Build();
var plugin = new HsmCertificateProviderPlugin();
Assert.That(plugin.CanCreateProvider(configuration), Is.False);
}
[Test]
public void CreateProvider_WithValidConfiguration_ReturnsProvider()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["hsm-slot"] = "0",
["hsm-pin"] = "1234",
["hsm-key-label"] = "signing-key"
})
.Build();
var plugin = new HsmCertificateProviderPlugin();
var provider = plugin.CreateProvider(configuration);
Assert.That(provider, Is.Not.Null);
Assert.That(provider, Is.InstanceOf<ICoseSigningKeyProvider>());
}
}❌ Bad:
public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null)
{
string clientSecret = "hardcoded-secret-value"; // NEVER DO THIS!
// ...
}✅ Good:
public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null)
{
// Read from configuration (environment variables, Azure Key Vault, etc.)
string? clientSecret = configuration["client-secret"];
// ...
}For Azure integrations, use DefaultAzureCredential with appropriate exclusions:
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
// Exclude interactive browser to prevent prompts in CI/CD
ExcludeInteractiveBrowserCredential = true
});public bool CanCreateProvider(IConfiguration configuration)
{
string? endpoint = configuration["endpoint"];
// Validate format
if (string.IsNullOrWhiteSpace(endpoint))
return false;
// Validate URL format
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out _))
return false;
// Validate scheme
if (!endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
return false;
return true;
}- Use
SecureStringfor passwords when possible - Clear sensitive data from memory after use
- Never log credentials or tokens
- Use Azure Key Vault or similar for secret storage
public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null)
{
string? pin = configuration["hsm-pin"];
try
{
// Use the PIN
var provider = new HsmProvider(pin);
return provider;
}
finally
{
// Clear sensitive data
if (pin != null)
{
// Zero out the string in memory if possible
// In production, consider using SecureString
}
}
}- Request only the permissions your plugin needs
- For Azure: Use specific RBAC roles, not Owner/Contributor
- For HSMs: Use dedicated slots/partitions with limited access
- Implement least privilege principle
public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null)
{
try
{
// Create provider
return new MyProvider(configuration);
}
catch (AuthenticationException ex)
{
// Don't expose sensitive auth details in error messages
logger?.LogError("Authentication failed. Verify credentials are configured correctly.");
throw new InvalidOperationException("Failed to authenticate with certificate provider.", ex);
}
catch (Exception ex)
{
logger?.LogError($"Unexpected error creating provider: {ex.Message}");
throw;
}
}- Log all signing operations (without sensitive data)
- Track who, what, when for compliance
- Monitor for unusual patterns
- Retain logs per compliance requirements
logger?.LogInformation($"Signing operation initiated for certificate profile: {profileName}");
logger?.LogInformation($"Certificate thumbprint: {cert.Thumbprint}");
// Never log: tokens, secrets, PINs, private keys- CoseSignTool.md - Main CoseSignTool documentation
- Plugins.md - General plugin development guide
- PluginNamingConventions.md - Plugin naming requirements
- CoseSign1.Certificates.AzureArtifactSigning.md - Azure Artifact Signing API documentation
- SCITTCompliance.md - SCITT compliance features