diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index c1cc4151..150f11a1 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -1,7 +1,7 @@
#### Build, Test, and Publish ####
# This is the main workflow for the CoseSignTool project. It handles the following events:
-# - Pull requests: When a user submits a pull request, or pushes a commit to an existing pull request, this workflow
-# - generates a changelog and commits it to the working branch, and then
+# - Pull requests: When a user submits a pull request, or pushes a commit to an existing pull request, this workflow
+# - generates a changelog and commits it to the working branch, and then
# - builds and tests the code.
# - Pushes to the main branch: When a user pushes a commit to the main branch, this workflow
# - creates a semantically versioned tag,
@@ -22,7 +22,7 @@ on:
jobs:
#### PULL REQUEST EVENTS ####
-
+
# Build and test the code.
build:
name: build-${{matrix.os}}${{matrix.runtime_id && format('-{0}', matrix.runtime_id) || ''}}
@@ -62,7 +62,7 @@ jobs:
dotnet build --configuration Debug CoseSignTool.sln
dotnet test --no-restore CoseSign1.Tests/CoseSign1.Tests.csproj
dotnet test --no-restore CoseSign1.Certificates.Tests/CoseSign1.Certificates.Tests.csproj
- dotnet test --no-restore CoseSign1.Certificates.AzureTrustedSigning.Tests/CoseSign1.Certificates.AzureTrustedSigning.Tests.csproj
+ dotnet test --no-restore CoseSign1.Certificates.AzureArtifactSigning.Tests/CoseSign1.Certificates.AzureArtifactSigning.Tests.csproj
dotnet test --no-restore CoseSign1.Headers.Tests/CoseSign1.Headers.Tests.csproj
dotnet test --no-restore CoseIndirectSignature.Tests/CoseIndirectSignature.Tests.csproj
dotnet test --no-restore CoseSign1.Transparent.Tests/CoseSign1.Transparent.Tests.csproj
@@ -73,7 +73,7 @@ jobs:
dotnet test --no-restore CoseSignTool.MST.Plugin.Tests/CoseSignTool.MST.Plugin.Tests.csproj
dotnet test --no-restore CoseSignTool.IndirectSignature.Plugin.Tests/CoseSignTool.IndirectSignature.Plugin.Tests.csproj
- # List the contents of the working directory to make sure all the artifacts are there.
+ # List the contents of the working directory to make sure all the artifacts are there.
- name: List working directory
run: ${{ matrix.dir_command }}
@@ -89,9 +89,7 @@ jobs:
permissions:
contents: write
steps:
-
#### PUSH TO MAIN — generate, commit, and push the changelog ####
-
- name: Checkout main
if: ${{ github.event_name == 'push' }}
uses: actions/checkout@v4
@@ -312,33 +310,33 @@ jobs:
# Remove the 'v' prefix from VERSION for VersionNgt property
VERSION_WITHOUT_V=$(echo "$VERSION" | sed 's/^v//')
RUNTIME_ID=${{ matrix.runtime_id }}
-
+
echo "Publishing single-file self-contained executable for runtime: $RUNTIME_ID"
echo "Plugins will be bundled inside the executable"
-
+
dotnet publish --no-restore --configuration Debug --self-contained true --runtime $RUNTIME_ID --output published/debug --property:FileVersion=$VERSION --property:VersionNgt=$VERSION_WITHOUT_V --property:PublishSingleFile=true CoseSignTool/CoseSignTool.csproj
dotnet publish --no-restore --configuration Release --self-contained true --runtime $RUNTIME_ID --output published/release --property:FileVersion=$VERSION --property:VersionNgt=$VERSION_WITHOUT_V --property:PublishSingleFile=true CoseSignTool/CoseSignTool.csproj
-
+
# PublishSingleFile=true bundles everything into a single executable:
# - The .NET runtime (self-contained)
# - All plugins (bundled and extracted on first run)
# - No separate plugins folder needed - everything is in the exe
shell: bash
-
+
# Verify the single-file executable was created correctly
# With PublishSingleFile=true and IncludeAllContentForSelfExtract=true, plugins are bundled INSIDE the exe
- name: Verify single-file executable
run: |
Write-Host "Verifying single-file self-contained executable..."
Write-Host ""
-
+
# Check debug output
Write-Host "=== Debug build ==="
$debugExe = Get-ChildItem "published/debug/CoseSignTool*" -File | Where-Object { $_.Extension -eq '.exe' -or $_.Extension -eq '' } | Select-Object -First 1
if ($debugExe) {
$sizeMB = [math]::Round($debugExe.Length / 1MB, 2)
Write-Host "✅ Found: $($debugExe.Name) ($sizeMB MB)"
-
+
# Plugins are bundled inside, so exe should be > 40MB (contains runtime + plugins)
if ($sizeMB -gt 40) {
Write-Host "✅ Size indicates plugins are bundled (expected for single-file with plugins)"
@@ -349,21 +347,21 @@ jobs:
Write-Host "❌ CoseSignTool executable not found in debug output!"
exit 1
}
-
+
# Check that plugins folder does NOT exist (should be bundled in exe)
if (Test-Path "published/debug/plugins") {
Write-Host "⚠️ Plugins folder exists - it should be cleaned up for single-file publish"
} else {
Write-Host "✅ No external plugins folder (correctly bundled in exe)"
}
-
+
Write-Host ""
Write-Host "=== Release build ==="
$releaseExe = Get-ChildItem "published/release/CoseSignTool*" -File | Where-Object { $_.Extension -eq '.exe' -or $_.Extension -eq '' } | Select-Object -First 1
if ($releaseExe) {
$sizeMB = [math]::Round($releaseExe.Length / 1MB, 2)
Write-Host "✅ Found: $($releaseExe.Name) ($sizeMB MB)"
-
+
if ($sizeMB -gt 40) {
Write-Host "✅ Size indicates plugins are bundled"
} else {
@@ -373,13 +371,13 @@ jobs:
Write-Host "❌ CoseSignTool executable not found in release output!"
exit 1
}
-
+
if (Test-Path "published/release/plugins") {
Write-Host "⚠️ Plugins folder exists - it should be cleaned up for single-file publish"
} else {
Write-Host "✅ No external plugins folder (correctly bundled in exe)"
}
-
+
Write-Host ""
Write-Host "Single-file verification complete. Plugins are bundled inside the executable."
Write-Host "On first run, the exe will extract to a temp directory including the plugins folder."
@@ -408,11 +406,11 @@ jobs:
- name: Create NuGet packages
run: |
echo "📦 Creating NuGet packages for library projects..."
-
+
VERSION=${{ needs.create_release.outputs.tag_name }}
# Remove the 'v' prefix from VERSION for VersionNgt property
VERSION_WITHOUT_V=$(echo "$VERSION" | sed 's/^v//')
-
+
# Define library projects that should be packaged (excluding plugins and test projects)
LIBRARY_PROJECTS=(
"CoseHandler/CoseHandler.csproj"
@@ -425,16 +423,16 @@ jobs:
"CoseSign1.Transparent.MST/CoseSign1.Transparent.MST.csproj"
"CoseSignTool.Abstractions/CoseSignTool.Abstractions.csproj"
)
-
+
# Create packages directory
mkdir -p published/packages
-
+
# Pack each library project
for project in "${LIBRARY_PROJECTS[@]}"; do
if [ -f "$project" ]; then
project_name=$(basename "${project%.*}")
echo "📦 Creating package for $project_name..."
-
+
dotnet pack "$project" \
--configuration Release \
--property:FileVersion=$VERSION \
@@ -442,7 +440,7 @@ jobs:
--property:VersionNgt=$VERSION_WITHOUT_V \
--output published/packages \
--verbosity minimal
-
+
if [ $? -eq 0 ]; then
echo "✅ Successfully created package for $project_name"
else
@@ -452,7 +450,7 @@ jobs:
echo "⚠️ Project file not found: $project"
fi
done
-
+
# List created packages
echo ""
echo "📋 Created NuGet packages:"
@@ -463,7 +461,7 @@ jobs:
else
echo "❌ No packages directory found"
fi
-
+
echo "🎯 NuGet package creation completed."
shell: bash
diff --git a/CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs
similarity index 72%
rename from CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs
rename to CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs
index d7cc0860..6e19b284 100644
--- a/CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs
+++ b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs
@@ -1,504 +1,506 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-namespace CoseSign1.Certificates.AzureTrustedSigning.Tests;
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using Azure.Developer.TrustedSigning.CryptoProvider;
-using CoseSign1.Certificates.AzureTrustedSigning;
-using CoseSign1.Certificates.Local;
-using CoseSign1.Tests.Common;
-using Moq;
-using NUnit.Framework;
-
-///
-/// Unit tests for the class.
-/// These tests ensure that the class behaves as expected under various conditions,
-/// including valid and invalid configurations of the Azure Trusted Signing context.
-///
-[TestFixture]
-public class AzureTrustedSigningCoseSigningKeyProviderTests
-{
- ///
- /// Tests the constructor to ensure it throws an when the sign context is null.
- ///
- [Test]
- public void Constructor_ThrowsArgumentNullException_WhenSignContextIsNull()
- {
- // Act & Assert
- Assert.That(
- () => new AzureTrustedSigningCoseSigningKeyProvider(null),
- Throws.TypeOf().With.Property("ParamName").EqualTo("signContext"));
- }
-
- ///
- /// Tests the method
- /// to ensure it throws an when the certificate chain is not available.
- ///
- [Test]
- public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateChainIsUnavailable()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns((IReadOnlyList?)null);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act & Assert
- Assert.That(
- () => InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList(),
- Throws.TypeOf().With.Message.Contains("Certificate chain is not available"));
- }
-
- ///
- /// Tests the method
- /// to ensure it throws an when the certificate chain is empty.
- ///
- [Test]
- public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateChainIsEmpty()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(new List());
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act & Assert
- Assert.That(
- () => InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList(),
- Throws.TypeOf().With.Message.Contains("Certificate chain is empty"));
- }
-
- ///
- /// Tests the method
- /// to ensure it returns the certificate chain in the correct order.
- ///
- /// The desired sort order of the certificate chain.
- /// Indicates whether the chain should be reversed.
- [Test]
- [TestCase(X509ChainSortOrder.LeafFirst, false, TestName = "GetCertificateChain_ReturnsChainInLeafFirstOrder")]
- [TestCase(X509ChainSortOrder.RootFirst, true, TestName = "GetCertificateChain_ReturnsChainInRootFirstOrder")]
- public void GetCertificateChain_ReturnsChainInCorrectOrder(X509ChainSortOrder sortOrder, bool reverseOrder)
- {
- // Arrange
- Mock mockSignContext = new Mock();
- List mockChain = CreateMockCertificateChain();
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(mockChain);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- List result = InvokeProtectedWithReflection>(provider, "GetCertificateChain", sortOrder).ToList();
-
- // Assert
- List expectedOrder = reverseOrder ? mockChain.AsEnumerable().Reverse().ToList() : mockChain;
- Assert.That(result, Is.EqualTo(expectedOrder));
- }
-
- ///
- /// Tests the method
- /// to ensure it throws an when the signing certificate is not available.
- ///
- [Test]
- public void GetSigningCertificate_ThrowsInvalidOperationException_WhenSigningCertificateIsUnavailable()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns((X509Certificate2?)null);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act & Assert
- Assert.That(
- () => InvokeProtectedWithReflection(provider, "GetSigningCertificate"),
- Throws.TypeOf().With.InnerException.TypeOf().And.InnerException.Message.Contains("Signing certificate is not available"));
- }
-
- ///
- /// Tests the method
- /// to ensure it returns the signing certificate.
- ///
- [Test]
- public void GetSigningCertificate_ReturnsSigningCertificate()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- X509Certificate2 mockCertificate = TestCertificateUtils.CreateCertificate(nameof(GetSigningCertificate_ReturnsSigningCertificate));
- mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns(mockCertificate);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- X509Certificate2 result = InvokeProtectedWithReflection(provider, "GetSigningCertificate");
-
- // Assert
- Assert.That(result, Is.EqualTo(mockCertificate));
- }
-
- ///
- /// Tests the method
- /// to ensure it throws a .
- ///
- [Test]
- public void ProvideECDsaKey_ThrowsNotSupportedException()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act & Assert
- Assert.That(
- () => InvokeProtectedWithReflection(provider, "ProvideECDsaKey", false),
- Throws.TypeOf().With.InnerException.TypeOf().And.InnerException.Message.Contains("ECDsa is not supported"));
- }
-
- ///
- /// Tests the method
- /// to ensure it returns an instance.
- ///
- [Test]
- public void ProvideRSAKey_ReturnsRSAInstance()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- RSA result = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
-
- // Assert
- Assert.That(result, Is.Not.Null);
- Assert.That(result, Is.InstanceOf());
- }
-
- ///
- /// Tests that caches the certificate chain
- /// and only calls GetCertChain on the context once.
- ///
- [Test]
- public void GetCertificateChain_CachesChainOnFirstCall()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- List mockChain = CreateMockCertificateChain();
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(mockChain);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- List result1 = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList();
- List result2 = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList();
-
- // Assert
- Assert.That(result1, Is.EqualTo(result2));
- mockSignContext.Verify(context => context.GetCertChain(It.IsAny()), Times.Once);
- }
-
- ///
- /// Tests that caches the certificate
- /// and only calls GetSigningCertificate on the context once.
- ///
- [Test]
- public void GetSigningCertificate_CachesCertificateOnFirstCall()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- X509Certificate2 mockCertificate = TestCertificateUtils.CreateCertificate(nameof(GetSigningCertificate_CachesCertificateOnFirstCall));
- mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns(mockCertificate);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- X509Certificate2 result1 = InvokeProtectedWithReflection(provider, "GetSigningCertificate");
- X509Certificate2 result2 = InvokeProtectedWithReflection(provider, "GetSigningCertificate");
-
- // Assert
- Assert.That(result1, Is.EqualTo(result2));
- mockSignContext.Verify(context => context.GetSigningCertificate(It.IsAny()), Times.Once);
- }
-
- ///
- /// Tests that caches the RSA instance
- /// and returns the same instance on subsequent calls.
- ///
- [Test]
- public void ProvideRSAKey_CachesRSAInstanceOnFirstCall()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- RSA result1 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
- RSA result2 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
-
- // Assert
- Assert.That(result1, Is.Not.Null);
- Assert.That(result2, Is.Not.Null);
- Assert.That(result1, Is.SameAs(result2), "ProvideRSAKey should return the same cached instance");
- }
-
- ///
- /// Tests that returns the same instance
- /// regardless of the publicKey parameter value (caching ignores the parameter).
- ///
- [Test]
- public void ProvideRSAKey_ReturnsSameCachedInstance_RegardlessOfPublicKeyParameter()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- RSA result1 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
- RSA result2 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", true);
-
- // Assert
- Assert.That(result1, Is.Not.Null);
- Assert.That(result2, Is.Not.Null);
- Assert.That(result1, Is.SameAs(result2), "ProvideRSAKey should return the same cached instance regardless of publicKey parameter");
- }
-
- ///
- /// Tests that is thread-safe
- /// and only calls GetCertChain once even under concurrent access.
- ///
- [Test]
- public void GetCertificateChain_IsThreadSafe_UnderConcurrentAccess()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- List mockChain = CreateMockCertificateChain();
- int callCount = 0;
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny()))
- .Returns(() =>
- {
- Interlocked.Increment(ref callCount);
- Thread.Sleep(10); // Simulate some delay to increase chance of race condition
- return mockChain;
- });
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act - Run multiple threads concurrently
- List>> tasks = new List>>();
- for (int i = 0; i < 10; i++)
- {
- tasks.Add(Task.Run(() =>
- InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList()));
- }
- Task.WaitAll(tasks.ToArray());
-
- // Assert
- Assert.That(callCount, Is.EqualTo(1), "GetCertChain should only be called once despite concurrent access");
- // Verify all tasks got the same chain
- List firstResult = tasks[0].Result;
- foreach (Task> task in tasks)
- {
- Assert.That(task.Result, Is.EqualTo(firstResult));
- }
- }
-
- ///
- /// Tests that GetCertificateChain correctly handles self-signed certificates (root certificates)
- /// where Issuer equals Subject.
- ///
- [Test]
- public void GetCertificateChain_HandlesRootCertificateCorrectly()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- // Create a certificate (CreateCertificate creates self-signed certs)
- X509Certificate2 rootCert = TestCertificateUtils.CreateCertificate(nameof(GetCertificateChain_HandlesRootCertificateCorrectly));
- List chain = new List { rootCert };
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(chain);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act - Request RootFirst for a self-signed cert
- List resultRootFirst = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.RootFirst).ToList();
-
- // Reset the chain cache by creating a new provider
- provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act - Request LeafFirst for a self-signed cert
- List resultLeafFirst = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList();
-
- // Assert - For self-signed cert, order should be reversed between RootFirst and LeafFirst
- Assert.That(resultRootFirst.Count, Is.EqualTo(1));
- Assert.That(resultLeafFirst.Count, Is.EqualTo(1));
- Assert.That(resultRootFirst[0], Is.EqualTo(chain[0]));
- Assert.That(resultLeafFirst[0], Is.EqualTo(chain[0]));
- }
-
- ///
- /// Tests that ProvideECDsaKey with publicKey=true still throws NotSupportedException.
- ///
- [Test]
- public void ProvideECDsaKey_ThrowsNotSupportedException_WithPublicKeyParameterTrue()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act & Assert
- Assert.That(
- () => InvokeProtectedWithReflection(provider, "ProvideECDsaKey", true),
- Throws.TypeOf().With.InnerException.TypeOf().And.InnerException.Message.Contains("ECDsa is not supported"));
- }
-
- ///
- /// Tests that ProvideRSAKey works correctly with publicKey parameter set to true.
- ///
- [Test]
- public void ProvideRSAKey_WorksWithPublicKeyParameterTrue()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- RSA result = InvokeProtectedWithReflection(provider, "ProvideRSAKey", true);
-
- // Assert
- Assert.That(result, Is.Not.Null);
- Assert.That(result, Is.InstanceOf());
- }
-
- ///
- /// Helper method to create a mock certificate chain.
- ///
- /// The name of the test method calling this helper.
- /// A list of mock objects.
- private static List CreateMockCertificateChain([CallerMemberName] string testCallerName = "")
- {
- return TestCertificateUtils.CreateTestChain(testCallerName).Cast().ToList();
- }
-
- ///
- /// Tests that the Issuer property returns a DID:X509:0 identifier with EKU format for non-standard EKUs.
- ///
- [Test]
- public void Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat()
- {
- // Arrange
- Mock mockSignContext = new Mock();
-
- // Create a certificate chain with non-standard EKU
- X509Certificate2 leafCert = TestCertificateUtils.CreateCertificate(nameof(Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat));
- X509Certificate2Collection chain = new() { leafCert };
-
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(chain.Cast().ToList());
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- string? issuer = provider.Issuer;
-
- // Assert
- Assert.That(issuer, Is.Not.Null.And.Not.Empty);
- Assert.That(issuer, Does.StartWith("did:x509:0:sha256:"));
- // Since test certs don't have non-standard EKUs, it should fall back to subject format
- // This tests that the Issuer property is accessible and returns a value
- // Base64url hash is 43 characters for SHA256 (per RFC 4648 Section 5)
- Assert.That(System.Text.RegularExpressions.Regex.IsMatch(issuer!, @"did:x509:0:sha256:[A-Za-z0-9_-]{43}::(subject|eku):"), Is.True);
- }
-
- ///
- /// Tests that the Issuer property returns null when certificate chain is unavailable.
- ///
- [Test]
- public void Issuer_WhenCertificateChainUnavailable_ReturnsNull()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns((IReadOnlyList?)null);
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- string? issuer = provider.Issuer;
-
- // Assert
- Assert.That(issuer, Is.Null);
- }
-
- ///
- /// Tests that the Issuer property returns null when certificate chain is empty.
- ///
- [Test]
- public void Issuer_WhenCertificateChainEmpty_ReturnsNull()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(new List());
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- string? issuer = provider.Issuer;
-
- // Assert
- Assert.That(issuer, Is.Null);
- }
-
- ///
- /// Tests that the Issuer property uses the Azure-specific DID generator.
- ///
- [Test]
- public void Issuer_UsesAzureTrustedSigningDidGenerator()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- X509Certificate2Collection chain = TestCertificateUtils.CreateTestChain(nameof(Issuer_UsesAzureTrustedSigningDidGenerator), leafFirst: true);
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(chain.Cast().ToList());
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- string? issuer = provider.Issuer;
-
- // Assert - Verify it generates a valid DID:X509:0 format
- Assert.That(issuer, Is.Not.Null.And.Not.Empty);
- Assert.That(issuer, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(issuer!.Contains("::subject:") || issuer.Contains("::eku:"), Is.True);
- }
-
- ///
- /// Tests that the Issuer property handles exceptions gracefully and falls back to base implementation.
- ///
- [Test]
- public void Issuer_OnException_FallsBackToBaseImplementation()
- {
- // Arrange
- Mock mockSignContext = new Mock();
- // Setup to throw an exception that should be caught
- mockSignContext.Setup(context => context.GetCertChain(It.IsAny()))
- .Throws(new InvalidOperationException("Test exception"));
- AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(mockSignContext.Object);
-
- // Act
- string? issuer = provider.Issuer;
-
- // Assert - Should fall back to base implementation which returns null on error
- Assert.That(issuer, Is.Null);
- }
-
- ///
- /// Helper method to invoke a protected method on the instance using reflection.
- ///
- /// The return type of the method being invoked.
- /// The instance of .
- /// The name of the protected method to invoke.
- /// The arguments to pass to the method.
- /// The result of the invoked method.
- private static T InvokeProtectedWithReflection(
- AzureTrustedSigningCoseSigningKeyProvider provider,
- string methodName,
- params object[] arguments)
- {
- // Use reflection to access the protected method
- MethodInfo method = typeof(AzureTrustedSigningCoseSigningKeyProvider)
- .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
-
- if (method == null)
- {
- throw new InvalidOperationException($"The protected method '{methodName}' could not be found.");
- }
-
- return (T)method.Invoke(provider, arguments);
- }
-}
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace CoseSign1.Certificates.AzureArtifactSigning.Tests;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Azure.Developer.ArtifactSigning.CryptoProvider;
+using Azure.Developer.ArtifactSigning.CryptoProvider.Interfaces;
+using CoseSign1.Certificates.AzureArtifactSigning;
+using CoseSign1.Certificates.Local;
+using CoseSign1.Tests.Common;
+using Moq;
+using NUnit.Framework;
+
+///
+/// Unit tests for the class.
+/// These tests ensure that the class behaves as expected under various conditions,
+/// including valid and invalid configurations of the Azure Artifact Signing context.
+///
+[TestFixture]
+public class AzureArtifactSigningCoseSigningKeyProviderTests
+{
+ ///
+ /// Tests the constructor to ensure it throws an when the sign context is null.
+ ///
+ [Test]
+ public void Constructor_ThrowsArgumentNullException_WhenSignContextIsNull()
+ {
+ // Act & Assert
+ Assert.That(
+ () => new AzureArtifactSigningCoseSigningKeyProvider(null),
+ Throws.TypeOf().With.Property("ParamName").EqualTo("ISignContext"));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it throws an when the certificate chain is not available.
+ ///
+ [Test]
+ public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateChainIsUnavailable()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns((IReadOnlyList?)null);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act & Assert
+ Assert.That(
+ () => InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList(),
+ Throws.TypeOf().With.Message.Contains("Certificate chain is not available"));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it throws an when the certificate chain is empty.
+ ///
+ [Test]
+ public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateChainIsEmpty()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(new List());
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act & Assert
+ Assert.That(
+ () => InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList(),
+ Throws.TypeOf().With.Message.Contains("Certificate chain is empty"));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it returns the certificate chain in the correct order.
+ ///
+ /// The desired sort order of the certificate chain.
+ /// Indicates whether the chain should be reversed.
+ [Test]
+ [TestCase(X509ChainSortOrder.LeafFirst, false, TestName = "GetCertificateChain_ReturnsChainInLeafFirstOrder")]
+ [TestCase(X509ChainSortOrder.RootFirst, true, TestName = "GetCertificateChain_ReturnsChainInRootFirstOrder")]
+ public void GetCertificateChain_ReturnsChainInCorrectOrder(X509ChainSortOrder sortOrder, bool reverseOrder)
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ List mockChain = CreateMockCertificateChain();
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(mockChain);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ List result = InvokeProtectedWithReflection>(provider, "GetCertificateChain", sortOrder).ToList();
+
+ // Assert
+ List expectedOrder = reverseOrder ? mockChain.AsEnumerable().Reverse().ToList() : mockChain;
+ Assert.That(result, Is.EqualTo(expectedOrder));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it throws an when the signing certificate is not available.
+ ///
+ [Test]
+ public void GetSigningCertificate_ThrowsInvalidOperationException_WhenSigningCertificateIsUnavailable()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns((X509Certificate2?)null);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act & Assert
+ Assert.That(
+ () => InvokeProtectedWithReflection(provider, "GetSigningCertificate"),
+ Throws.TypeOf().With.InnerException.TypeOf().And.InnerException.Message.Contains("Signing certificate is not available"));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it returns the signing certificate.
+ ///
+ [Test]
+ public void GetSigningCertificate_ReturnsSigningCertificate()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ X509Certificate2 mockCertificate = TestCertificateUtils.CreateCertificate(nameof(GetSigningCertificate_ReturnsSigningCertificate));
+ mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns(mockCertificate);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ X509Certificate2 result = InvokeProtectedWithReflection(provider, "GetSigningCertificate");
+
+ // Assert
+ Assert.That(result, Is.EqualTo(mockCertificate));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it throws a .
+ ///
+ [Test]
+ public void ProvideECDsaKey_ThrowsNotSupportedException()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act & Assert
+ Assert.That(
+ () => InvokeProtectedWithReflection(provider, "ProvideECDsaKey", false),
+ Throws.TypeOf().With.InnerException.TypeOf().And.InnerException.Message.Contains("ECDsa is not supported"));
+ }
+
+ ///
+ /// Tests the method
+ /// to ensure it returns an instance.
+ ///
+ [Test]
+ public void ProvideRSAKey_ReturnsRSAInstance()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ RSA result = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result, Is.InstanceOf());
+ }
+
+ ///
+ /// Tests that caches the certificate chain
+ /// and only calls GetCertChain on the context once.
+ ///
+ [Test]
+ public void GetCertificateChain_CachesChainOnFirstCall()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ List mockChain = CreateMockCertificateChain();
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(mockChain);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ List result1 = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList();
+ List result2 = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList();
+
+ // Assert
+ Assert.That(result1, Is.EqualTo(result2));
+ mockSignContext.Verify(context => context.GetCertChain(It.IsAny()), Times.Once);
+ }
+
+ ///
+ /// Tests that caches the certificate
+ /// and only calls GetSigningCertificate on the context once.
+ ///
+ [Test]
+ public void GetSigningCertificate_CachesCertificateOnFirstCall()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ X509Certificate2 mockCertificate = TestCertificateUtils.CreateCertificate(nameof(GetSigningCertificate_CachesCertificateOnFirstCall));
+ mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns(mockCertificate);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ X509Certificate2 result1 = InvokeProtectedWithReflection(provider, "GetSigningCertificate");
+ X509Certificate2 result2 = InvokeProtectedWithReflection(provider, "GetSigningCertificate");
+
+ // Assert
+ Assert.That(result1, Is.EqualTo(result2));
+ mockSignContext.Verify(context => context.GetSigningCertificate(It.IsAny()), Times.Once);
+ }
+
+ ///
+ /// Tests that caches the RSA instance
+ /// and returns the same instance on subsequent calls.
+ ///
+ [Test]
+ public void ProvideRSAKey_CachesRSAInstanceOnFirstCall()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ RSA result1 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
+ RSA result2 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
+
+ // Assert
+ Assert.That(result1, Is.Not.Null);
+ Assert.That(result2, Is.Not.Null);
+ Assert.That(result1, Is.SameAs(result2), "ProvideRSAKey should return the same cached instance");
+ }
+
+ ///
+ /// Tests that returns the same instance
+ /// regardless of the publicKey parameter value (caching ignores the parameter).
+ ///
+ [Test]
+ public void ProvideRSAKey_ReturnsSameCachedInstance_RegardlessOfPublicKeyParameter()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ RSA result1 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", false);
+ RSA result2 = InvokeProtectedWithReflection(provider, "ProvideRSAKey", true);
+
+ // Assert
+ Assert.That(result1, Is.Not.Null);
+ Assert.That(result2, Is.Not.Null);
+ Assert.That(result1, Is.SameAs(result2), "ProvideRSAKey should return the same cached instance regardless of publicKey parameter");
+ }
+
+ ///
+ /// Tests that is thread-safe
+ /// and only calls GetCertChain once even under concurrent access.
+ ///
+ [Test]
+ public void GetCertificateChain_IsThreadSafe_UnderConcurrentAccess()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ List mockChain = CreateMockCertificateChain();
+ int callCount = 0;
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny()))
+ .Returns(() =>
+ {
+ Interlocked.Increment(ref callCount);
+ Thread.Sleep(10); // Simulate some delay to increase chance of race condition
+ return mockChain;
+ });
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act - Run multiple threads concurrently
+ List>> tasks = new List>>();
+ for (int i = 0; i < 10; i++)
+ {
+ tasks.Add(Task.Run(() =>
+ InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList()));
+ }
+ Task.WaitAll(tasks.ToArray());
+
+ // Assert
+ Assert.That(callCount, Is.EqualTo(1), "GetCertChain should only be called once despite concurrent access");
+ // Verify all tasks got the same chain
+ List firstResult = tasks[0].Result;
+ foreach (Task> task in tasks)
+ {
+ Assert.That(task.Result, Is.EqualTo(firstResult));
+ }
+ }
+
+ ///
+ /// Tests that GetCertificateChain correctly handles self-signed certificates (root certificates)
+ /// where Issuer equals Subject.
+ ///
+ [Test]
+ public void GetCertificateChain_HandlesRootCertificateCorrectly()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ // Create a certificate (CreateCertificate creates self-signed certs)
+ X509Certificate2 rootCert = TestCertificateUtils.CreateCertificate(nameof(GetCertificateChain_HandlesRootCertificateCorrectly));
+ List chain = new List { rootCert };
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(chain);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act - Request RootFirst for a self-signed cert
+ List resultRootFirst = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.RootFirst).ToList();
+
+ // Reset the chain cache by creating a new provider
+ provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act - Request LeafFirst for a self-signed cert
+ List resultLeafFirst = InvokeProtectedWithReflection>(provider, "GetCertificateChain", X509ChainSortOrder.LeafFirst).ToList();
+
+ // Assert - For self-signed cert, order should be reversed between RootFirst and LeafFirst
+ Assert.That(resultRootFirst.Count, Is.EqualTo(1));
+ Assert.That(resultLeafFirst.Count, Is.EqualTo(1));
+ Assert.That(resultRootFirst[0], Is.EqualTo(chain[0]));
+ Assert.That(resultLeafFirst[0], Is.EqualTo(chain[0]));
+ }
+
+ ///
+ /// Tests that ProvideECDsaKey with publicKey=true still throws NotSupportedException.
+ ///
+ [Test]
+ public void ProvideECDsaKey_ThrowsNotSupportedException_WithPublicKeyParameterTrue()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act & Assert
+ Assert.That(
+ () => InvokeProtectedWithReflection(provider, "ProvideECDsaKey", true),
+ Throws.TypeOf().With.InnerException.TypeOf().And.InnerException.Message.Contains("ECDsa is not supported"));
+ }
+
+ ///
+ /// Tests that ProvideRSAKey works correctly with publicKey parameter set to true.
+ ///
+ [Test]
+ public void ProvideRSAKey_WorksWithPublicKeyParameterTrue()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ RSA result = InvokeProtectedWithReflection(provider, "ProvideRSAKey", true);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result, Is.InstanceOf());
+ }
+
+ ///
+ /// Helper method to create a mock certificate chain.
+ ///
+ /// The name of the test method calling this helper.
+ /// A list of mock objects.
+ private static List CreateMockCertificateChain([CallerMemberName] string testCallerName = "")
+ {
+ return TestCertificateUtils.CreateTestChain(testCallerName).Cast().ToList();
+ }
+
+ ///
+ /// Tests that the Issuer property returns a DID:X509:0 identifier with EKU format for non-standard EKUs.
+ ///
+ [Test]
+ public void Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+
+ // Create a certificate chain with non-standard EKU
+ X509Certificate2 leafCert = TestCertificateUtils.CreateCertificate(nameof(Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat));
+ X509Certificate2Collection chain = new() { leafCert };
+
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(chain.Cast().ToList());
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ string? issuer = provider.Issuer;
+
+ // Assert
+ Assert.That(issuer, Is.Not.Null.And.Not.Empty);
+ Assert.That(issuer, Does.StartWith("did:x509:0:sha256:"));
+ // Since test certs don't have non-standard EKUs, it should fall back to subject format
+ // This tests that the Issuer property is accessible and returns a value
+ // Base64url hash is 43 characters for SHA256 (per RFC 4648 Section 5)
+ Assert.That(System.Text.RegularExpressions.Regex.IsMatch(issuer!, @"did:x509:0:sha256:[A-Za-z0-9_-]{43}::(subject|eku):"), Is.True);
+ }
+
+ ///
+ /// Tests that the Issuer property returns null when certificate chain is unavailable.
+ ///
+ [Test]
+ public void Issuer_WhenCertificateChainUnavailable_ReturnsNull()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns((IReadOnlyList?)null);
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ string? issuer = provider.Issuer;
+
+ // Assert
+ Assert.That(issuer, Is.Null);
+ }
+
+ ///
+ /// Tests that the Issuer property returns null when certificate chain is empty.
+ ///
+ [Test]
+ public void Issuer_WhenCertificateChainEmpty_ReturnsNull()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(new List());
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ string? issuer = provider.Issuer;
+
+ // Assert
+ Assert.That(issuer, Is.Null);
+ }
+
+ ///
+ /// Tests that the Issuer property uses the Azure-specific DID generator.
+ ///
+ [Test]
+ public void Issuer_UsesAzureArtifactSigningDidGenerator()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ X509Certificate2Collection chain = TestCertificateUtils.CreateTestChain(nameof(Issuer_UsesAzureArtifactSigningDidGenerator), leafFirst: true);
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(chain.Cast().ToList());
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ string? issuer = provider.Issuer;
+
+ // Assert - Verify it generates a valid DID:X509:0 format
+ Assert.That(issuer, Is.Not.Null.And.Not.Empty);
+ Assert.That(issuer, Does.StartWith("did:x509:0:sha256:"));
+ Assert.That(issuer!.Contains("::subject:") || issuer.Contains("::eku:"), Is.True);
+ }
+
+ ///
+ /// Tests that the Issuer property handles exceptions gracefully and falls back to base implementation.
+ ///
+ [Test]
+ public void Issuer_OnException_FallsBackToBaseImplementation()
+ {
+ // Arrange
+ Mock mockSignContext = new Mock();
+ // Setup to throw an exception that should be caught
+ mockSignContext.Setup(context => context.GetCertChain(It.IsAny()))
+ .Throws(new InvalidOperationException("Test exception"));
+ AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object);
+
+ // Act
+ string? issuer = provider.Issuer;
+
+ // Assert - Should fall back to base implementation which returns null on error
+ Assert.That(issuer, Is.Null);
+ }
+
+ ///
+ /// Helper method to invoke a protected method on the instance using reflection.
+ ///
+ /// The return type of the method being invoked.
+ /// The instance of .
+ /// The name of the protected method to invoke.
+ /// The arguments to pass to the method.
+ /// The result of the invoked method.
+ private static T InvokeProtectedWithReflection(
+ AzureArtifactSigningCoseSigningKeyProvider provider,
+ string methodName,
+ params object[] arguments)
+ {
+ // Use reflection to access the protected method
+ MethodInfo method = typeof(AzureArtifactSigningCoseSigningKeyProvider)
+ .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
+
+ if (method == null)
+ {
+ throw new InvalidOperationException($"The protected method '{methodName}' could not be found.");
+ }
+
+ return (T)method.Invoke(provider, arguments);
+ }
+}
diff --git a/CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningDidX509GeneratorTests.cs b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningDidX509GeneratorTests.cs
similarity index 90%
rename from CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningDidX509GeneratorTests.cs
rename to CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningDidX509GeneratorTests.cs
index 30cb4211..fedd22c7 100644
--- a/CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningDidX509GeneratorTests.cs
+++ b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningDidX509GeneratorTests.cs
@@ -1,650 +1,649 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-namespace CoseSign1.Certificates.AzureTrustedSigning.Tests;
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using System.Text.RegularExpressions;
-using CoseSign1.Certificates.AzureTrustedSigning;
-using CoseSign1.Tests.Common;
-using NUnit.Framework;
-
-///
-/// Unit tests for the class.
-/// Tests Azure Trusted Signing specific DID generation with Microsoft EKU support.
-///
-[TestFixture]
-public class AzureTrustedSigningDidX509GeneratorTests
-{
- private AzureTrustedSigningDidX509Generator _generator = null!;
-
- [SetUp]
- public void SetUp()
- {
- _generator = new AzureTrustedSigningDidX509Generator();
- }
-
- #region GenerateFromChain Tests
-
- [Test]
- public void GenerateFromChain_WithNullCertificates_ThrowsArgumentNullException()
- {
- // Act & Assert
- ArgumentNullException ex = Assert.Throws(() => _generator.GenerateFromChain(null!));
- Assert.That(ex.ParamName, Is.EqualTo("certificates"));
- }
-
- [Test]
- public void GenerateFromChain_WithEmptyChain_ThrowsArgumentException()
- {
- // Arrange
- X509Certificate2[] emptyChain = Array.Empty();
-
- // Act & Assert
- ArgumentException ex = Assert.Throws(() => _generator.GenerateFromChain(emptyChain));
- Assert.That(ex.Message, Does.Contain("cannot be empty"));
- Assert.That(ex.ParamName, Is.EqualTo("certificates"));
- }
-
- [Test]
- public void GenerateFromChain_WithOnlyStandardEkus_UsesSuperClassImplementation()
- {
- // Arrange - Standard EKUs (no Microsoft EKUs)
- using X509Certificate2 leafCert = CreateCertificateWithStandardEkus("CN=Leaf");
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should use subject-based format (no Microsoft EKUs present)
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain("::subject:"));
- Assert.That(did, Does.Not.Contain("::eku:"));
- }
-
- [Test]
- public void GenerateFromChain_WithNoEkus_UsesSuperClassImplementation()
- {
- // Arrange - Certificate without any EKUs
- using X509Certificate2 leafCert = CreateCertificateWithoutEku("CN=Leaf");
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should use subject-based format (no Microsoft EKUs present)
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain("::subject:"));
- Assert.That(did, Does.Not.Contain("::eku:"));
- }
-
- [Test]
- public void GenerateFromChain_WithMicrosoftEku_UsesEkuBasedFormat()
- {
- // Arrange - Azure Trusted Signing certificate with Microsoft EKU
- string microsoftEku = "1.3.6.1.4.1.311.20.30.40.50";
- using X509Certificate2 leafCert = CreateCertificateWithCustomEku("CN=Leaf", microsoftEku);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should use EKU-based format (Azure Trusted Signing specific)
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain($"::eku:{microsoftEku}"));
- Assert.That(did, Does.Not.Contain("::subject:"));
- }
-
- [Test]
- public void GenerateFromChain_WithMixedEkus_SelectsDeepestMicrosoftEku()
- {
- // Arrange - Mix of standard and Microsoft EKUs
- List ekus = new()
- {
- "1.3.6.1.5.5.7.3.1", // Standard TLS Server Auth (not Microsoft)
- "1.3.6.1.4.1.311.20.30.40.50", // Microsoft EKU (9 segments)
- "1.3.6.1.4.1.311.99.88.77.66.55" // Microsoft EKU (11 segments - deepest)
- };
- using X509Certificate2 leafCert = CreateCertificateWithMultipleEkus("CN=Leaf", ekus);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should select the deepest Microsoft EKU
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain("::eku:1.3.6.1.4.1.311.99.88.77.66.55"));
- }
-
- [Test]
- public void GenerateFromChain_WithMultipleMicrosoftEkusOfSameDepth_SelectsGreatestLastSegment()
- {
- // Arrange - Multiple Microsoft EKUs with same depth
- List ekus = new()
- {
- "1.3.6.1.4.1.311.20.5", // Last segment: 5
- "1.3.6.1.4.1.311.20.99", // Last segment: 99 (greatest)
- "1.3.6.1.4.1.311.20.50" // Last segment: 50
- };
- using X509Certificate2 leafCert = CreateCertificateWithMultipleEkus("CN=Leaf", ekus);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should select Microsoft EKU with greatest last segment value
- Assert.That(did, Does.Contain("::eku:1.3.6.1.4.1.311.20.99"));
- }
-
- [Test]
- public void GenerateFromChain_WithSingleMicrosoftEku_SelectsThatEku()
- {
- // Arrange - Single Microsoft EKU
- string microsoftEku = "1.3.6.1.4.1.311.99.88.77";
- using X509Certificate2 leafCert = CreateCertificateWithCustomEku("CN=Leaf", microsoftEku);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert
- Assert.That(did, Does.Contain($"::eku:{microsoftEku}"));
- }
-
- [Test]
- public void GenerateFromChain_IncludesCorrectRootHash()
- {
- // Arrange
- string customEku = "1.3.6.1.4.1.311.88.77.66";
- using X509Certificate2 leafCert = CreateCertificateWithCustomEku("CN=Leaf", customEku);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Extract hash from DID and verify it's correct format
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(Regex.IsMatch(did, @"did:x509:0:sha256:[A-Za-z0-9_-]{43}::eku:1\.3\.6\.1\.4\.1\.311\.\d+\.\d+\.\d+"), Is.True);
-
- // The hash should be from the root cert (self-signed one) in base64url format (43 chars for SHA256)
- string hashPart = did.Substring("did:x509:0:sha256:".Length, 43);
- Assert.That(hashPart, Has.Length.EqualTo(43));
- Assert.That(Regex.IsMatch(hashPart, "^[A-Za-z0-9_-]{43}$"), Is.True);
- }
-
- [Test]
- public void GenerateFromChain_WithChainContainingSelfSignedCertificate_UsesItAsRoot()
- {
- // Arrange
- string customEku = "1.3.6.1.4.1.311.77.88";
- using X509Certificate2 leafCert = CreateCertificateWithCustomEku("CN=Leaf", customEku);
- using X509Certificate2 intermediateCert = CreateCertificateWithoutEku("CN=Intermediate");
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, intermediateCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should use valid format with EKU
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain("::eku:"));
-
- // The hash should be from a root cert in the chain in base64url format (43 chars for SHA256)
- string hashPart = did.Substring("did:x509:0:sha256:".Length, 43);
- Assert.That(hashPart, Has.Length.EqualTo(43));
- Assert.That(Regex.IsMatch(hashPart, "^[A-Za-z0-9_-]{43}$"), Is.True);
- }
-
- [Test]
- public void GenerateFromChain_WithNonMicrosoftEku_UsesSuperClassImplementation()
- {
- // Arrange - Non-Microsoft EKU (not Azure Trusted Signing)
- string nonMicrosoftEku = "1.2.3.4.5.6.7.8.9";
- using X509Certificate2 leafCert = CreateCertificateWithCustomEku("CN=Leaf", nonMicrosoftEku);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should use subject-based format (no Microsoft EKUs)
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain("::subject:"));
- Assert.That(did, Does.Not.Contain("::eku:"));
- }
-
- [Test]
- public void GenerateFromChain_WithMixOfMicrosoftAndNonMicrosoftEkus_UsesOnlyMicrosoftEkus()
- {
- // Arrange - Mix of Microsoft and non-Microsoft EKUs
- List ekus = new()
- {
- "1.2.3.4.5.6.7.8.9.10.11.12", // Non-Microsoft (12 segments but ignored)
- "1.3.6.1.4.1.311.20.30", // Microsoft EKU (9 segments)
- "1.3.6.1.4.1.311.40.50.60" // Microsoft EKU (10 segments - selected)
- };
- using X509Certificate2 leafCert = CreateCertificateWithMultipleEkus("CN=Leaf", ekus);
- using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root");
- X509Certificate2[] chain = new[] { leafCert, rootCert };
-
- // Act
- string did = _generator.GenerateFromChain(chain);
-
- // Assert - Should select the deepest Microsoft EKU only
- Assert.That(did, Does.StartWith("did:x509:0:sha256:"));
- Assert.That(did, Does.Contain("::eku:1.3.6.1.4.1.311.40.50.60"));
- Assert.That(did, Does.Not.Contain("1.2.3.4.5"));
- }
-
- #endregion
-
- #region ExtractEkus Tests
-
- [Test]
- public void ExtractEkus_WithNoEkuExtension_ReturnsEmptyList()
- {
- // Arrange
- using X509Certificate2 cert = CreateCertificateWithoutEku("CN=Test");
-
- // Act
- List ekus = InvokeProtectedMethod>("ExtractEkus", cert);
-
- // Assert
- Assert.That(ekus, Is.Empty);
- }
-
- [Test]
- public void ExtractEkus_WithSingleEku_ReturnsSingleEku()
- {
- // Arrange
- string expectedEku = "1.2.3.4.5";
- using X509Certificate2 cert = CreateCertificateWithCustomEku("CN=Test", expectedEku);
-
- // Act
- List ekus = InvokeProtectedMethod>("ExtractEkus", cert);
-
- // Assert
- Assert.That(ekus, Has.Count.EqualTo(1));
- Assert.That(ekus[0], Is.EqualTo(expectedEku));
- }
-
- [Test]
- public void ExtractEkus_WithMultipleEkus_ReturnsAllEkus()
- {
- // Arrange - Use valid OID format
- List expectedEkus = new() { "1.2.3.4.5", "1.2.3.4.6", "1.2.3.4.7" };
- using X509Certificate2 cert = CreateCertificateWithMultipleEkus("CN=Test", expectedEkus);
-
- // Act
- List ekus = InvokeProtectedMethod>("ExtractEkus", cert);
-
- // Assert
- Assert.That(ekus, Is.EquivalentTo(expectedEkus));
- }
-
- [Test]
- public void ExtractEkus_WithStandardEkus_ReturnsStandardEkus()
- {
- // Arrange
- using X509Certificate2 cert = CreateCertificateWithStandardEkus("CN=Test");
-
- // Act
- List ekus = InvokeProtectedMethod>("ExtractEkus", cert);
-
- // Assert
- Assert.That(ekus, Is.Not.Empty);
- Assert.That(ekus.Any(eku => eku == "1.3.6.1.5.5.7.3.1" || eku == "1.3.6.1.5.5.7.3.2"), Is.True);
- }
-
- #endregion
-
- #region SelectDeepestGreatestEku Tests
-
- [Test]
- public void SelectDeepestGreatestEku_WithEmptyList_ThrowsArgumentException()
- {
- // Arrange
- List emptyList = new();
-
- // Act & Assert - Unwrap TargetInvocationException
- var ex = Assert.Throws(() =>
- InvokeProtectedMethod("SelectDeepestGreatestEku", emptyList));
-
- Assert.That(ex.InnerException, Is.TypeOf());
- ArgumentException argEx = (ArgumentException)ex.InnerException!;
- Assert.That(argEx.Message, Does.Contain("cannot be empty"));
- Assert.That(argEx.ParamName, Is.EqualTo("ekus"));
- }
-
- [Test]
- public void SelectDeepestGreatestEku_WithSingleEku_ReturnsThatEku()
- {
- // Arrange
- List ekus = new() { "1.3.6.1.4.1.311.20.30" };
-
- // Act
- string result = InvokeProtectedMethod("SelectDeepestGreatestEku", ekus);
-
- // Assert
- Assert.That(result, Is.EqualTo("1.3.6.1.4.1.311.20.30"));
- }
-
- [Test]
- public void SelectDeepestGreatestEku_WithDifferentDepths_ReturnsDeepest()
- {
- // Arrange
- List ekus = new()
- {
- "1.3.6.1.4.1.311", // 7 segments
- "1.3.6.1.4.1.311.20.30.40.50.60.70", // 13 segments (deepest)
- "1.3.6.1.4.1.311.88" // 8 segments
- };
-
- // Act
- string result = InvokeProtectedMethod("SelectDeepestGreatestEku", ekus);
-
- // Assert
- Assert.That(result, Is.EqualTo("1.3.6.1.4.1.311.20.30.40.50.60.70"));
- }
-
- [Test]
- public void SelectDeepestGreatestEku_WithSameDepth_ReturnsGreatestLastSegment()
- {
- // Arrange
- List ekus = new()
- {
- "1.3.6.1.4.1.311.20.5", // Last segment: 5
- "1.3.6.1.4.1.311.20.100", // Last segment: 100 (greatest)
- "1.3.6.1.4.1.311.20.42" // Last segment: 42
- };
-
- // Act
- string result = InvokeProtectedMethod("SelectDeepestGreatestEku", ekus);
-
- // Assert
- Assert.That(result, Is.EqualTo("1.3.6.1.4.1.311.20.100"));
- }
-
- [Test]
- public void SelectDeepestGreatestEku_WithSameDepthAndLastSegment_ReturnsFirst()
- {
- // Arrange - When depth and last segment are identical, first wins
- List ekus = new()
- {
- "1.3.6.1.4.1.311.20.5",
- "1.3.6.1.4.1.311.88.5", // Same depth and last segment
- "1.3.6.1.4.1.311.99.5" // Same depth and last segment
- };
-
- // Act
- string result = InvokeProtectedMethod("SelectDeepestGreatestEku", ekus);
-
- // Assert
- Assert.That(result, Is.EqualTo("1.3.6.1.4.1.311.20.5"));
- }
-
- [Test]
- public void SelectDeepestGreatestEku_PrioritizesDepthOverLastSegment()
- {
- // Arrange
- List ekus = new()
- {
- "1.3.6.1.4.1.311.999", // 8 segments, huge last segment
- "1.3.6.1.4.1.311.20.30.40.50.1" // 11 segments, small last segment (should win due to depth)
- };
-
- // Act
- string result = InvokeProtectedMethod("SelectDeepestGreatestEku", ekus);
-
- // Assert
- Assert.That(result, Is.EqualTo("1.3.6.1.4.1.311.20.30.40.50.1"));
- }
-
- #endregion
-
- #region CountSegments Tests
-
- [Test]
- public void CountSegments_WithValidOid_ReturnsCorrectCount()
- {
- // Arrange & Act & Assert
- Assert.That(InvokeProtectedMethod("CountSegments", "1.2.3.4.5"), Is.EqualTo(5));
- Assert.That(InvokeProtectedMethod("CountSegments", "1.2"), Is.EqualTo(2));
- Assert.That(InvokeProtectedMethod("CountSegments", "1"), Is.EqualTo(1));
- Assert.That(InvokeProtectedMethod