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
42 changes: 42 additions & 0 deletions CoseSign1.Abstractions/Interfaces/ISupportsScittCompliance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace CoseSign1.Abstractions.Interfaces;

/// <summary>
/// Optional capability interface implemented by <see cref="ICoseSigningKeyProvider"/> implementations
/// that support toggling SCITT (Supply Chain Integrity, Transparency, and Trust) compliance behavior.
/// </summary>
/// <remarks>
/// <para>
/// Hosts that need to enable or disable automatic CWT (CBOR Web Token) claim emission on a
/// signing-key provider should test for this interface rather than depending on a specific concrete
/// type. This keeps the cross-boundary contract in <c>CoseSign1.Abstractions</c>, allowing
/// implementing assemblies (e.g. <c>CoseSign1.Certificates</c>) to remain plugin-local in
/// host/plugin <see cref="System.Runtime.Loader.AssemblyLoadContext"/> isolation scenarios without
/// triggering type-identity mismatches.
/// </para>
/// <para>
/// Example host pattern:
/// <code>
/// ICoseSigningKeyProvider provider = plugin.CreateProvider(configuration);
/// if (provider is ISupportsScittCompliance scittProvider)
/// {
/// scittProvider.EnableScittCompliance = enableScittCompliance;
/// }
/// </code>
/// </para>
/// </remarks>
public interface ISupportsScittCompliance
{
/// <summary>
/// Gets or sets a value indicating whether SCITT-compliant CWT claims (issuer and subject) are
/// automatically added to signatures produced by this provider.
/// </summary>
/// <remarks>
/// Implementations should default this to <c>true</c> when SCITT compliance is the expected
/// behavior for the provider. Setting to <c>false</c> suppresses default-claim emission;
/// user-supplied CWT claims attached via header extenders remain unaffected.
/// </remarks>
bool EnableScittCompliance { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace CoseSign1.Certificates.AzureArtifactSigning.Tests;

using System;
using Azure.CodeSigning;
using Azure.Core;
using Azure.Developer.ArtifactSigning.CryptoProvider.Models;
using NUnit.Framework;

/// <summary>
/// Tests for <see cref="AasClientOptionsExtensions"/> — the AAS analogue of the MST
/// performance optimisation extensions. These verify that interactive signing latency
/// is bounded by short, fixed-delay retries and that callers can override defaults
/// without losing fluent chaining.
/// </summary>
[TestFixture]
public class AasClientOptionsExtensionsTests
{
[Test]
public void ConfigureAasPerformanceOptimizations_AppliesFastFixedRetries()
{
// Arrange
CertificateProfileClientOptions options = new CertificateProfileClientOptions();

// Act
CertificateProfileClientOptions returned = options.ConfigureAasPerformanceOptimizations();

// Assert
Assert.That(returned, Is.SameAs(options), "Extension method must return the same instance for fluent chaining.");
Assert.That(options.Retry.Mode, Is.EqualTo(RetryMode.Fixed), "Retry mode must be Fixed for predictable interactive latency.");
Assert.That(options.Retry.Delay, Is.EqualTo(AasClientOptionsExtensions.DefaultRetryDelay));
Assert.That(options.Retry.MaxRetries, Is.EqualTo(AasClientOptionsExtensions.DefaultMaxRetries));
}

[Test]
public void ConfigureAasPerformanceOptimizations_HonoursOverrides()
{
// Arrange
CertificateProfileClientOptions options = new CertificateProfileClientOptions();
TimeSpan customDelay = TimeSpan.FromMilliseconds(100);
const int customRetries = 16;

// Act
options.ConfigureAasPerformanceOptimizations(customDelay, customRetries);

// Assert
Assert.That(options.Retry.Delay, Is.EqualTo(customDelay));
Assert.That(options.Retry.MaxRetries, Is.EqualTo(customRetries));
}

[Test]
public void ConfigureAasPerformanceOptimizations_NullOptions_Throws()
{
// Act / Assert
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(
() => AasClientOptionsExtensions.ConfigureAasPerformanceOptimizations(null!));
Assert.That(ex.ParamName, Is.EqualTo("options"));
}

[Test]
public void ConfigureAasSigningPerformance_ReturnsDefaultsWhenUnset()
{
// Act
AzSignContextOptions opts = AasClientOptionsExtensions.ConfigureAasSigningPerformance();

// Assert
Assert.That(opts, Is.Not.Null);
Assert.That(opts.TaskRetryCount, Is.EqualTo(AasClientOptionsExtensions.DefaultSigningTaskRetryCount));
Assert.That(opts.TaskTimeOutInSeconds, Is.EqualTo(AasClientOptionsExtensions.DefaultSigningTaskTimeoutSeconds));
}

[Test]
public void ConfigureAasSigningPerformance_HonoursOverrides()
{
// Act
AzSignContextOptions opts = AasClientOptionsExtensions.ConfigureAasSigningPerformance(
taskRetryCount: 5,
taskTimeoutSeconds: 30);

// Assert
Assert.That(opts.TaskRetryCount, Is.EqualTo(5));
Assert.That(opts.TaskTimeOutInSeconds, Is.EqualTo(30));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.CodeSigning;

using System;
using Azure.Core;
using Azure.Developer.ArtifactSigning.CryptoProvider.Models;

/// <summary>
/// Extension methods for tuning the Azure Artifact Signing (AAS) client pipeline and signing-context
/// timing knobs for improved interactive signing performance.
/// </summary>
/// <remarks>
/// <para>
/// The Azure SDK's default <see cref="RetryOptions"/> use exponential back-off starting at 800 ms with
/// 3 retries (≈ 5 s before giving up). For interactive signing scenarios — where a user is waiting at
/// a console — that can stretch a transient blip into a multi-second wait. These extensions apply
/// fixed-delay 250 ms retries with up to 8 attempts so the typical recovery window is short, while
/// leaving the <c>AzSignContext</c> long-running signing operation knobs
/// (<see cref="AzSignContextOptions"/>) adjustable via <see cref="ConfigureAasSigningPerformance"/>.
/// </para>
/// <para>
/// <b>Retry-After honoured.</b> Azure.Core's <c>RetryPolicy</c> always honours a server-supplied
/// <c>Retry-After</c> header even when <see cref="RetryMode.Fixed"/> is configured. If AAS asks the
/// client to back off (e.g. <c>Retry-After: 30</c>), that value wins over the configured 250 ms delay,
/// so the practical worst-case latency is bounded by the server's policy, not the SDK ceiling. This is
/// intentional: AAS uses <c>Retry-After</c> correctly and the client should respect it. Unlike the MST
/// equivalent (<c>MstClientOptionsExtensions.ConfigureMstPerformanceOptimizations</c>) this helper does
/// <b>not</b> strip <c>Retry-After</c> headers — that is an MST-specific work-around for the Code
/// Transparency Service's eventual-consistency window where the server returns optimistic 1-second
/// hints that the client can safely beat.
/// </para>
/// </remarks>
public static class AasClientOptionsExtensions
{
/// <summary>
/// The default interval between fast retry attempts when the server does not supply
/// <c>Retry-After</c>. A server-supplied value will override this.
/// </summary>
public static readonly TimeSpan DefaultRetryDelay = TimeSpan.FromMilliseconds(250);

/// <summary>
/// The default maximum number of fast retry attempts. With no <c>Retry-After</c> back-pressure
/// the worst-case ceiling is approximately <c>MaxRetries * DefaultRetryDelay</c> (≈ 2 seconds).
/// </summary>
public const int DefaultMaxRetries = 8;

/// <summary>
/// The default <see cref="AzSignContextOptions.TaskRetryCount"/> applied by
/// <see cref="ConfigureAasSigningPerformance"/>. Matches the SDK default; surfaced as a constant
/// so callers can tune relative to a known baseline.
/// </summary>
public const int DefaultSigningTaskRetryCount = 3;

/// <summary>
/// The default <see cref="AzSignContextOptions.TaskTimeOutInSeconds"/> applied by
/// <see cref="ConfigureAasSigningPerformance"/>. Matches the SDK default.
/// </summary>
public const int DefaultSigningTaskTimeoutSeconds = 60;

/// <summary>
/// Configures the Azure SDK retry pipeline on a <see cref="CertificateProfileClientOptions"/>
/// instance to use a short fixed delay between retries so transient certificate-profile lookups
/// do not stretch interactive signing latency. Server-supplied <c>Retry-After</c> values are
/// still honoured and override the configured delay.
/// </summary>
/// <param name="options">The <see cref="CertificateProfileClientOptions"/> to configure.</param>
/// <param name="retryDelay">
/// Interval between fast retry attempts. Defaults to <see cref="DefaultRetryDelay"/> (250 ms).
/// </param>
/// <param name="maxRetries">
/// Maximum number of fast retry attempts before failing. Defaults to <see cref="DefaultMaxRetries"/> (8).
/// </param>
/// <returns>The same <paramref name="options"/> instance for fluent chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
/// <example>
/// <code>
/// CertificateProfileClientOptions options = new CertificateProfileClientOptions();
/// options.ConfigureAasPerformanceOptimizations();
/// CertificateProfileClient client = new CertificateProfileClient(credential, endpoint, options);
/// </code>
/// </example>
public static CertificateProfileClientOptions ConfigureAasPerformanceOptimizations(
this CertificateProfileClientOptions options,
TimeSpan? retryDelay = null,
int? maxRetries = null)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

options.Retry.Mode = RetryMode.Fixed;
options.Retry.Delay = retryDelay ?? DefaultRetryDelay;
options.Retry.MaxRetries = maxRetries ?? DefaultMaxRetries;

return options;
}

/// <summary>
/// Builds an <see cref="AzSignContextOptions"/> instance with the supplied long-running signing
/// timing knobs, falling back to the SDK defaults when a value is not specified.
/// </summary>
/// <param name="taskRetryCount">
/// Maximum number of times the signing task is retried before failing. Defaults to
/// <see cref="DefaultSigningTaskRetryCount"/>.
/// </param>
/// <param name="taskTimeoutSeconds">
/// Per-task timeout in seconds for the signing long-running operation. Defaults to
/// <see cref="DefaultSigningTaskTimeoutSeconds"/>.
/// </param>
/// <returns>A populated <see cref="AzSignContextOptions"/>.</returns>
/// <example>
/// <code>
/// AzSignContextOptions opts = AasClientOptionsExtensions.ConfigureAasSigningPerformance(
/// taskRetryCount: 5,
/// taskTimeoutSeconds: 30);
/// AzSignContext signContext = new AzSignContext(account, profile, client, null, opts);
/// </code>
/// </example>
public static AzSignContextOptions ConfigureAasSigningPerformance(
int? taskRetryCount = null,
int? taskTimeoutSeconds = null)
{
return new AzSignContextOptions
{
TaskRetryCount = taskRetryCount ?? DefaultSigningTaskRetryCount,
TaskTimeOutInSeconds = taskTimeoutSeconds ?? DefaultSigningTaskTimeoutSeconds
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,62 @@ public void TestGetProtectedHeaders_WithScittEnabled_IncludesDefaultCWTClaims()
claims!.Issuer.Should().NotBeNull();
claims.Subject.Should().Be(CwtClaims.DefaultSubject);
}

/// <summary>
/// Verifies that <see cref="CertificateCoseSigningKeyProvider"/> implements the shared
/// <see cref="ISupportsScittCompliance"/> capability interface from
/// <c>CoseSign1.Abstractions</c>. This is the cross-AssemblyLoadContext contract that lets
/// the host toggle SCITT compliance on plugin-returned providers without referencing the
/// concrete provider type — keeping <c>CoseSign1.Certificates</c> plugin-local.
/// </summary>
[Test]
public void TestImplementsISupportsScittCompliance_ContractDefinedInAbstractions()
{
// Arrange
X509Certificate2 cert = TestCertificateUtils.CreateCertificate();
ICertificateChainBuilder chainBuilder = new TestChainBuilder();
ICoseSigningKeyProvider provider = new X509Certificate2CoseSigningKeyProvider(chainBuilder, cert);

// Act — host pattern: probe via shared capability interface, never via concrete type.
bool implementsScittCapability = provider is ISupportsScittCompliance;

// Assert
implementsScittCapability.Should().BeTrue(
"CertificateCoseSigningKeyProvider must expose the shared ISupportsScittCompliance capability so the host can toggle SCITT compliance without depending on the concrete type");

// The capability interface MUST live in CoseSign1.Abstractions so it can be host-shared
// across plugin AssemblyLoadContext boundaries. Verifying this stops accidental moves
// into a plugin-local assembly.
typeof(ISupportsScittCompliance).Assembly.GetName().Name.Should().Be(
"CoseSign1.Abstractions",
"ISupportsScittCompliance must remain in the host-shared CoseSign1.Abstractions assembly to preserve cross-ALC type identity");
}

/// <summary>
/// Verifies the round-trip: setting EnableScittCompliance through the
/// <see cref="ISupportsScittCompliance"/> interface flows through to the underlying provider's
/// behavior (matching the <c>SignCommand</c> downcast pattern).
/// </summary>
[Test]
public void TestISupportsScittCompliance_RoundTripsThroughInterface()
{
// Arrange
X509Certificate2 cert = TestCertificateUtils.CreateCertificate();
ICertificateChainBuilder chainBuilder = new TestChainBuilder();
ICoseSigningKeyProvider provider = new X509Certificate2CoseSigningKeyProvider(chainBuilder, cert);

// Act — exact mirror of SignCommand's host-side pattern after the Phase 3 refactor.
if (provider is ISupportsScittCompliance scittProvider)
{
scittProvider.EnableScittCompliance = false;
}

// Assert — concrete property reflects the interface-driven change.
((CertificateCoseSigningKeyProvider)provider).EnableScittCompliance.Should().BeFalse(
"Setting EnableScittCompliance via the shared interface must affect the underlying provider behavior");

CoseHeaderMap headers = provider.GetProtectedHeaders();
headers.ContainsKey(CWTClaimsHeaderLabels.CWTClaims).Should().BeFalse(
"Disabling SCITT compliance through the shared interface must suppress default CWT claim emission");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace CoseSign1.Certificates;
/// <summary>
/// Abstract class which contains common logic needed for all certificate based <see cref="ICoseSigningKeyProvider"/> implementations.
/// </summary>
public abstract class CertificateCoseSigningKeyProvider : ICoseSigningKeyProvider
public abstract class CertificateCoseSigningKeyProvider : ICoseSigningKeyProvider, ISupportsScittCompliance
{
private static readonly DidX509Generator DefaultDidGenerator = new();

Expand Down
Loading
Loading