Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [Unreleased](https://github.com/microsoft/CoseSignTool/tree/HEAD)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.6.9...HEAD)

**Merged pull requests:**

- Add --payload-location option for IndirectSignature [\#158](https://github.com/microsoft/CoseSignTool/pull/158) ([JeromySt](https://github.com/JeromySt))
- Enabled CodeQL scans [\#156](https://github.com/microsoft/CoseSignTool/pull/156) ([NN2000X](https://github.com/NN2000X))

## [v1.6.9](https://github.com/microsoft/CoseSignTool/tree/v1.6.9) (2025-12-17)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.6.8...v1.6.9)
Expand Down
178 changes: 178 additions & 0 deletions CoseSignTool.Abstractions.Tests/PluginInterfaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,184 @@ public async Task TestPluginCommand_ExecuteAsync_ThrowsException_PropagatesExcep
await Assert.ThrowsExceptionAsync<InvalidOperationException>(
() => command.ExecuteAsync(configuration));
}

/// <summary>
/// Tests that TestPluginCommand BooleanOptions returns empty collection by default.
/// </summary>
[TestMethod]
public void TestPluginCommand_BooleanOptions_ReturnsEmptyByDefault()
{
// Arrange & Act
TestPluginCommand command = new TestPluginCommand();

// Assert
Assert.IsNotNull(command.BooleanOptions);
Assert.AreEqual(0, command.BooleanOptions.Count);
}

/// <summary>
/// Tests GetBooleanFlag returns false when key is not present.
/// </summary>
[TestMethod]
public void GetBooleanFlag_KeyNotPresent_ReturnsFalse()
{
// Arrange
Dictionary<string, string?> configData = new Dictionary<string, string?>();
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Act
bool result = TestablePluginCommand.TestGetBooleanFlag(configuration, "nonexistent");

// Assert
Assert.IsFalse(result);
}

/// <summary>
/// Tests GetBooleanFlag returns true when key is present with empty value.
/// </summary>
[TestMethod]
public void GetBooleanFlag_KeyPresentWithEmptyValue_ReturnsTrue()
{
// Arrange
Dictionary<string, string?> configData = new Dictionary<string, string?>
{
{ "my-flag", "" }
};
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Act
bool result = TestablePluginCommand.TestGetBooleanFlag(configuration, "my-flag");

// Assert
Assert.IsTrue(result);
}

/// <summary>
/// Tests GetBooleanFlag returns true when key has value "true".
/// </summary>
[TestMethod]
public void GetBooleanFlag_KeyPresentWithTrueValue_ReturnsTrue()
{
// Arrange
Dictionary<string, string?> configData = new Dictionary<string, string?>
{
{ "my-flag", "true" }
};
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Act
bool result = TestablePluginCommand.TestGetBooleanFlag(configuration, "my-flag");

// Assert
Assert.IsTrue(result);
}

/// <summary>
/// Tests GetBooleanFlag returns true when key has any non-false value.
/// </summary>
[TestMethod]
public void GetBooleanFlag_KeyPresentWithAnyNonFalseValue_ReturnsTrue()
{
// Arrange
Dictionary<string, string?> configData = new Dictionary<string, string?>
{
{ "my-flag", "anything" }
};
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Act
bool result = TestablePluginCommand.TestGetBooleanFlag(configuration, "my-flag");

// Assert
Assert.IsTrue(result);
}

/// <summary>
/// Tests GetBooleanFlag returns false when key has value "false".
/// </summary>
[TestMethod]
public void GetBooleanFlag_KeyPresentWithFalseValue_ReturnsFalse()
{
// Arrange
Dictionary<string, string?> configData = new Dictionary<string, string?>
{
{ "my-flag", "false" }
};
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Act
bool result = TestablePluginCommand.TestGetBooleanFlag(configuration, "my-flag");

// Assert
Assert.IsFalse(result);
}

/// <summary>
/// Tests GetBooleanFlag returns false when key has value "FALSE" (case insensitive).
/// </summary>
[TestMethod]
public void GetBooleanFlag_KeyPresentWithFalseValueUpperCase_ReturnsFalse()
{
// Arrange
Dictionary<string, string?> configData = new Dictionary<string, string?>
{
{ "my-flag", "FALSE" }
};
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

// Act
bool result = TestablePluginCommand.TestGetBooleanFlag(configuration, "my-flag");

// Assert
Assert.IsFalse(result);
}
}

/// <summary>
/// Testable version of PluginCommandBase that exposes protected methods for testing.
/// </summary>
public class TestablePluginCommand : PluginCommandBase
{
/// <inheritdoc/>
public override string Name => "testable";

/// <inheritdoc/>
public override string Description => "Testable command";

/// <inheritdoc/>
public override string Usage => "testable [options]";

/// <inheritdoc/>
public override IDictionary<string, string> Options => new Dictionary<string, string>();

/// <inheritdoc/>
public override Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default)
{
return Task.FromResult(PluginExitCode.Success);
}

/// <summary>
/// Exposes the protected GetBooleanFlag method for testing.
/// </summary>
/// <param name="configuration">The configuration to check.</param>
/// <param name="key">The key to look for.</param>
/// <returns>True if the flag is set, false otherwise.</returns>
public static bool TestGetBooleanFlag(IConfiguration configuration, string key)
{
return GetBooleanFlag(configuration, key);
}
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions CoseSignTool.Abstractions/IPluginCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public interface IPluginCommand
/// </summary>
IDictionary<string, string> Options { get; }

/// <summary>
/// Gets the names of options that are boolean flags (can be specified without a value).
/// When these options are specified without an explicit value, they are treated as "true".
/// </summary>
IReadOnlyCollection<string> BooleanOptions { get; }

/// <summary>
/// Executes the command with the provided configuration.
/// </summary>
Expand Down
44 changes: 44 additions & 0 deletions CoseSignTool.Abstractions/PluginCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public void SetLogger(IPluginLogger logger)
/// <inheritdoc/>
public abstract IDictionary<string, string> Options { get; }

/// <inheritdoc/>
/// <remarks>
/// Override this property in derived classes to specify which options are boolean flags.
/// Boolean flags can be specified without an explicit value (e.g., --verbose instead of --verbose true).
/// </remarks>
public virtual IReadOnlyCollection<string> BooleanOptions => Array.Empty<string>();

/// <inheritdoc/>
public abstract Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default);

Expand Down Expand Up @@ -58,4 +65,41 @@ protected static string GetRequiredValue(IConfiguration configuration, string ke
{
return configuration[key] ?? defaultValue;
}

/// <summary>
/// Gets a boolean flag from configuration. Handles CLI switches that may not have explicit values.
/// Returns true if the flag is present (with any non-"false" value or no value), false otherwise.
/// </summary>
/// <param name="configuration">The configuration to read from.</param>
/// <param name="key">The configuration key.</param>
/// <returns>True if the flag is set, false otherwise.</returns>
/// <remarks>
/// This method handles the common CLI pattern where a flag can be specified in multiple ways:
/// --flag (no value, implies true)
/// --flag true (explicit true)
/// --flag false (explicit false)
/// The flag is considered true if:
/// - The key exists in configuration AND
/// - The value is not explicitly "false" (case-insensitive)
/// </remarks>
protected static bool GetBooleanFlag(IConfiguration configuration, string key)
{
// Check if the key exists in the configuration
string? value = configuration[key];

// If the key doesn't exist, return false
if (value == null)
{
return false;
}

// If the value is explicitly "false", return false
if (string.Equals(value, "false", StringComparison.OrdinalIgnoreCase))
{
return false;
}

// Otherwise, the flag was specified (with any value or no value), so return true
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ public void IndirectSignCommand_Options_ShouldContainRequiredOptions()
Assert.IsTrue(options.ContainsKey("hash-algorithm"));
}

[TestMethod]
public void IndirectSignCommand_BooleanOptions_ShouldContainSigningBooleanOptions()
{
// Arrange
IndirectSignCommand command = new IndirectSignCommand();

// Act
IReadOnlyCollection<string> booleanOptions = command.BooleanOptions;

// Assert
Assert.IsNotNull(booleanOptions);
Assert.IsTrue(booleanOptions.Contains("enable-scitt"), "BooleanOptions should contain 'enable-scitt'");
}

[TestMethod]
public async Task IndirectSignCommand_Execute_WithValidPfxCertificate_ShouldSucceed()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ public void IndirectVerifyCommand_Options_ShouldContainRequiredOptions()
Assert.IsTrue(options.ContainsKey("common-name"));
}

[TestMethod]
public void IndirectVerifyCommand_BooleanOptions_ShouldContainValidationBooleanOptions()
{
// Arrange
IndirectVerifyCommand command = new IndirectVerifyCommand();

// Act
IReadOnlyCollection<string> booleanOptions = command.BooleanOptions;

// Assert
Assert.IsNotNull(booleanOptions);
Assert.IsTrue(booleanOptions.Contains("allow-untrusted"), "BooleanOptions should contain 'allow-untrusted'");
Assert.IsTrue(booleanOptions.Contains("allow-outdated"), "BooleanOptions should contain 'allow-outdated'");
}

[TestMethod]
public async Task IndirectVerifyCommand_Execute_WithValidSignature_ShouldSucceed()
{
Expand Down
15 changes: 15 additions & 0 deletions CoseSignTool.IndirectSignature.Plugin/IndirectSignCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ public override IDictionary<string, string> Options
GetCertificateProviderInfo() +
GetExamples();

/// <inheritdoc/>
public override IReadOnlyCollection<string> BooleanOptions => SigningBooleanOptions;

/// <summary>
/// Gets additional optional arguments specific to the indirect-sign command.
/// </summary>
protected override string GetAdditionalOptionalArguments()
{
return @"
Payload location (optional):
--payload-location A URI indicating where the payload can be retrieved from.
This is added to the COSE envelope per RFC 9054.
";
}

/// <inheritdoc/>
public override async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ public abstract class IndirectSignatureCommandBase : PluginCommandBase
{ "revocation-mode", "Certificate revocation checking mode (NoCheck, Online, Offline, default: NoCheck)" }
};

/// <summary>
/// Boolean options that can be specified without an explicit value.
/// These are common to verification commands.
/// </summary>
protected static readonly string[] ValidationBooleanOptions = new[]
{
"allow-untrusted",
"allow-outdated"
};

/// <summary>
/// Boolean options for signing commands (SCITT-related flags).
/// </summary>
protected static readonly string[] SigningBooleanOptions = new[]
{
"enable-scitt"
};

/// <summary>
/// Header options for customizing COSE headers.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public override IDictionary<string, string> Options
GetAdditionalOptionalArguments() +
GetExamples();

/// <inheritdoc/>
public override IReadOnlyCollection<string> BooleanOptions => ValidationBooleanOptions;

/// <inheritdoc/>
public override async Task<PluginExitCode> ExecuteAsync(IConfiguration configuration, CancellationToken cancellationToken = default)
{
Expand All @@ -48,8 +51,8 @@ public override async Task<PluginExitCode> ExecuteAsync(IConfiguration configura
// Get optional parameters
string? outputPath = GetOptionalValue(configuration, "output");
string? rootCertsPath = GetOptionalValue(configuration, "roots");
bool allowUntrusted = !string.IsNullOrEmpty(GetOptionalValue(configuration, "allow-untrusted"));
bool allowOutdated = !string.IsNullOrEmpty(GetOptionalValue(configuration, "allow-outdated"));
bool allowUntrusted = GetBooleanFlag(configuration, "allow-untrusted");
bool allowOutdated = GetBooleanFlag(configuration, "allow-outdated");
string? commonName = GetOptionalValue(configuration, "common-name");
string? revocationModeString = GetOptionalValue(configuration, "revocation-mode");

Expand Down
Loading
Loading