From 947d2a09beda631e413cac3029c18dcb1cd84413 Mon Sep 17 00:00:00 2001 From: Jaxel Rojas Lopez Date: Tue, 10 Mar 2026 22:43:55 -0400 Subject: [PATCH 1/4] refactor: rebrand trusted signing plugin to artifact signing --- ...ifactSigningCoseSigningKeyProviderTests.cs | 1008 ++++++------- ...ureArtifactSigningDidX509GeneratorTests.cs | 1299 ++++++++--------- ...ificates.AzureArtifactSigning.Tests.csproj | 64 +- ...reArtifactSigningCoseSigningKeyProvider.cs | 257 ++-- .../AzureArtifactSigningDidX509Generator.cs | 436 +++--- ...1.Certificates.AzureArtifactSigning.csproj | 90 +- .../ICertificateProviderPlugin.cs | 8 +- ...rtifactSigningCertificateProviderPlugin.cs | 436 +++--- ...ignTool.AzureArtifactSigning.Plugin.csproj | 0 .../Usings.cs | 12 +- .../IndirectSignCommand.cs | 50 +- CoseSignTool.sln | 8 +- ...ign1.Certificates.AzureArtifactSigning.md} | 478 +++--- docs/Plugins.md | 2 +- 14 files changed, 2073 insertions(+), 2075 deletions(-) rename CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs => CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs (78%) rename CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningDidX509GeneratorTests.cs => CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningDidX509GeneratorTests.cs (90%) rename CoseSign1.Certificates.AzureTrustedSigning.Tests/CoseSign1.Certificates.AzureTrustedSigning.Tests.csproj => CoseSign1.Certificates.AzureArtifactSigning.Tests/CoseSign1.Certificates.AzureArtifactSigning.Tests.csproj (85%) rename CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs => CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs (76%) rename CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs => CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningDidX509Generator.cs (88%) rename CoseSign1.Certificates.AzureTrustedSigning/CoseSign1.Certificates.AzureTrustedSigning.csproj => CoseSign1.Certificates.AzureArtifactSigning/CoseSign1.Certificates.AzureArtifactSigning.csproj (85%) rename CoseSignTool.AzureTrustedSigning.Plugin/AzureTrustedSigningCertificateProviderPlugin.cs => CoseSignTool.AzureArtifactSigning.Plugin/AzureArtifactSigningCertificateProviderPlugin.cs (61%) rename CoseSignTool.AzureTrustedSigning.Plugin/CoseSignTool.AzureTrustedSigning.Plugin.csproj => CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj (100%) rename {CoseSignTool.AzureTrustedSigning.Plugin => CoseSignTool.AzureArtifactSigning.Plugin}/Usings.cs (97%) rename docs/{CoseSign1.Certificates.AzureTrustedSigning.md => CoseSign1.Certificates.AzureArtifactSigning.md} (78%) diff --git a/CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs similarity index 78% rename from CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs rename to CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs index d7cc0860..79ce2476 100644 --- a/CoseSign1.Certificates.AzureTrustedSigning.Tests/AzureTrustedSigningCoseSigningKeyProviderTests.cs +++ b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs @@ -1,504 +1,504 @@ -// 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 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("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); + 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("CountSegments", "1.2.3.4.5.6.7.8.9.10"), Is.EqualTo(10)); - } - - [Test] - public void CountSegments_WithEmptyString_ReturnsZero() - { - // Act - int result = InvokeProtectedMethod("CountSegments", string.Empty); - - // Assert - Assert.That(result, Is.EqualTo(0)); - } - - [Test] - public void CountSegments_WithNull_ReturnsZero() - { - // Act - int result = InvokeProtectedMethod("CountSegments", new object[] { null! }); - - // Assert - Assert.That(result, Is.EqualTo(0)); - } - - #endregion - - #region GetLastSegmentValue Tests - - [Test] - public void GetLastSegmentValue_WithValidOid_ReturnsLastSegmentValue() - { - // Arrange & Act & Assert - Assert.That(InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.5"), Is.EqualTo(5)); - Assert.That(InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.999"), Is.EqualTo(999)); - Assert.That(InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.0"), Is.EqualTo(0)); - } - - [Test] - public void GetLastSegmentValue_WithSingleSegment_ReturnsValue() - { - // Act - long result = InvokeProtectedMethod("GetLastSegmentValue", "42"); - - // Assert - Assert.That(result, Is.EqualTo(42)); - } - - [Test] - public void GetLastSegmentValue_WithEmptyString_ReturnsZero() - { - // Act - long result = InvokeProtectedMethod("GetLastSegmentValue", string.Empty); - - // Assert - Assert.That(result, Is.EqualTo(0)); - } - - [Test] - public void GetLastSegmentValue_WithNull_ReturnsZero() - { - // Act - long result = InvokeProtectedMethod("GetLastSegmentValue", new object[] { null! }); - - // Assert - Assert.That(result, Is.EqualTo(0)); - } - - [Test] - public void GetLastSegmentValue_WithNonNumericLastSegment_ReturnsZero() - { - // Act - If the last segment isn't numeric, should return 0 - long result = InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.abc"); - - // Assert - Assert.That(result, Is.EqualTo(0)); - } - - [Test] - public void GetLastSegmentValue_WithLargeNumber_ReturnsCorrectValue() - { - // Arrange - long expectedValue = 9223372036854775807; // long.MaxValue - - // Act - long result = InvokeProtectedMethod("GetLastSegmentValue", $"1.2.3.4.{expectedValue}"); - - // Assert - Assert.That(result, Is.EqualTo(expectedValue)); - } - - #endregion - - #region Standard EKU Coverage Tests - - [Test] - public void GenerateFromChain_WithOnlyNonMicrosoftStandardEkus_UsesSuperClass() - { - // Arrange - Only non-Microsoft standard EKUs - List standardEkus = new() - { - "1.3.6.1.5.5.7.3.1", // TLS Server Authentication - "1.3.6.1.5.5.7.3.2", // TLS Client Authentication - "1.3.6.1.5.5.7.3.3", // Code Signing - "1.3.6.1.5.5.7.3.4", // Email Protection - "1.3.6.1.5.5.7.3.8" // Time Stamping - }; - - using X509Certificate2 leafCert = CreateCertificateWithMultipleEkus("CN=Leaf", standardEkus); - using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root"); - X509Certificate2[] chain = new[] { leafCert, rootCert }; - - // Act - string did = _generator.GenerateFromChain(chain); - - // Assert - Should use subject format (no Microsoft EKUs) - Assert.That(did, Does.Contain("::subject:")); - Assert.That(did, Does.Not.Contain("::eku:")); - } - - [Test] - public void GenerateFromChain_WithMicrosoftLifetimeSigningEku_UsesEkuFormat() - { - // Arrange - Lifetime Signing is a Microsoft EKU that triggers Azure Trusted Signing format - List ekus = new() - { - "1.3.6.1.5.5.7.3.3", // Code Signing (standard, non-Microsoft) - "1.3.6.1.4.1.311.10.3.13" // Lifetime Signing (Microsoft EKU) - }; - - 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 use EKU format (Microsoft EKU present) - Assert.That(did, Does.Contain("::eku:1.3.6.1.4.1.311.10.3.13")); - Assert.That(did, Does.Not.Contain("::subject:")); - } - - #endregion - - #region Helper Methods - - private T InvokeProtectedMethod(string methodName, params object[] args) - { - var method = typeof(AzureTrustedSigningDidX509Generator) - .GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - if (method == null) - { - throw new InvalidOperationException($"Method {methodName} not found"); - } - - return (T)method.Invoke(_generator, args)!; - } - - private X509Certificate2 CreateCertificateWithoutEku(string subject) - { - using RSA rsa = RSA.Create(2048); - CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - // Add basic constraints but no EKU - request.CertificateExtensions.Add( - new X509BasicConstraintsExtension(false, false, 0, true)); - - return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); - } - - private X509Certificate2 CreateCertificateWithStandardEkus(string subject) - { - using RSA rsa = RSA.Create(2048); - CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - OidCollection oids = new() - { - new Oid("1.3.6.1.5.5.7.3.1"), // TLS Server auth - new Oid("1.3.6.1.5.5.7.3.2") // TLS Client auth - }; - - request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, false)); - - return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); - } - - private X509Certificate2 CreateCertificateWithCustomEku(string subject, string ekuOid) - { - using RSA rsa = RSA.Create(2048); - CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - OidCollection oids = new() { new Oid(ekuOid) }; - request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, false)); - - return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); - } - - private X509Certificate2 CreateCertificateWithMultipleEkus(string subject, List ekuOids) - { - using RSA rsa = RSA.Create(2048); - CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - OidCollection oids = new(); - foreach (string oid in ekuOids) - { - oids.Add(new Oid(oid)); - } - - request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, false)); - - return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); - } - - private X509Certificate2 CreateSelfSignedCertificate(string subject) - { - using RSA rsa = RSA.Create(2048); - CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - // Mark as CA - request.CertificateExtensions.Add( - new X509BasicConstraintsExtension(true, false, 0, true)); - - return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); - } - - #endregion -} +// 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.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using CoseSign1.Certificates.AzureArtifactSigning; +using NUnit.Framework; + +/// +/// Unit tests for the class. +/// Tests Azure Artifact Signing specific DID generation with Microsoft EKU support. +/// +[TestFixture] +public class AzureArtifactSigningDidX509GeneratorTests +{ + private AzureArtifactSigningDidX509Generator Generator = null!; + + [SetUp] + public void SetUp() + { + Generator = new AzureArtifactSigningDidX509Generator(); + } + + #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 Artifact 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 Artifact 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 Artifact 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("CountSegments", "1.2.3.4.5.6.7.8.9.10"), Is.EqualTo(10)); + } + + [Test] + public void CountSegments_WithEmptyString_ReturnsZero() + { + // Act + int result = InvokeProtectedMethod("CountSegments", string.Empty); + + // Assert + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void CountSegments_WithNull_ReturnsZero() + { + // Act + int result = InvokeProtectedMethod("CountSegments", new object[] { null! }); + + // Assert + Assert.That(result, Is.EqualTo(0)); + } + + #endregion + + #region GetLastSegmentValue Tests + + [Test] + public void GetLastSegmentValue_WithValidOid_ReturnsLastSegmentValue() + { + // Arrange & Act & Assert + Assert.That(InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.5"), Is.EqualTo(5)); + Assert.That(InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.999"), Is.EqualTo(999)); + Assert.That(InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.0"), Is.EqualTo(0)); + } + + [Test] + public void GetLastSegmentValue_WithSingleSegment_ReturnsValue() + { + // Act + long result = InvokeProtectedMethod("GetLastSegmentValue", "42"); + + // Assert + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void GetLastSegmentValue_WithEmptyString_ReturnsZero() + { + // Act + long result = InvokeProtectedMethod("GetLastSegmentValue", string.Empty); + + // Assert + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void GetLastSegmentValue_WithNull_ReturnsZero() + { + // Act + long result = InvokeProtectedMethod("GetLastSegmentValue", new object[] { null! }); + + // Assert + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void GetLastSegmentValue_WithNonNumericLastSegment_ReturnsZero() + { + // Act - If the last segment isn't numeric, should return 0 + long result = InvokeProtectedMethod("GetLastSegmentValue", "1.2.3.4.abc"); + + // Assert + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void GetLastSegmentValue_WithLargeNumber_ReturnsCorrectValue() + { + // Arrange + long expectedValue = 9223372036854775807; // long.MaxValue + + // Act + long result = InvokeProtectedMethod("GetLastSegmentValue", $"1.2.3.4.{expectedValue}"); + + // Assert + Assert.That(result, Is.EqualTo(expectedValue)); + } + + #endregion + + #region Standard EKU Coverage Tests + + [Test] + public void GenerateFromChain_WithOnlyNonMicrosoftStandardEkus_UsesSuperClass() + { + // Arrange - Only non-Microsoft standard EKUs + List standardEkus = new() + { + "1.3.6.1.5.5.7.3.1", // TLS Server Authentication + "1.3.6.1.5.5.7.3.2", // TLS Client Authentication + "1.3.6.1.5.5.7.3.3", // Code Signing + "1.3.6.1.5.5.7.3.4", // Email Protection + "1.3.6.1.5.5.7.3.8" // Time Stamping + }; + + using X509Certificate2 leafCert = CreateCertificateWithMultipleEkus("CN=Leaf", standardEkus); + using X509Certificate2 rootCert = CreateSelfSignedCertificate("CN=Root"); + X509Certificate2[] chain = new[] { leafCert, rootCert }; + + // Act + string did = Generator.GenerateFromChain(chain); + + // Assert - Should use subject format (no Microsoft EKUs) + Assert.That(did, Does.Contain("::subject:")); + Assert.That(did, Does.Not.Contain("::eku:")); + } + + [Test] + public void GenerateFromChain_WithMicrosoftLifetimeSigningEku_UsesEkuFormat() + { + // Arrange - Lifetime Signing is a Microsoft EKU that triggers Azure Artifact Signing format + List ekus = new() + { + "1.3.6.1.5.5.7.3.3", // Code Signing (standard, non-Microsoft) + "1.3.6.1.4.1.311.10.3.13" // Lifetime Signing (Microsoft EKU) + }; + + 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 use EKU format (Microsoft EKU present) + Assert.That(did, Does.Contain("::eku:1.3.6.1.4.1.311.10.3.13")); + Assert.That(did, Does.Not.Contain("::subject:")); + } + + #endregion + + #region Helper Methods + + private T InvokeProtectedMethod(string methodName, params object[] args) + { + var method = typeof(AzureArtifactSigningDidX509Generator) + .GetMethod(methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + if (method == null) + { + throw new InvalidOperationException($"Method {methodName} not found"); + } + + return (T)method.Invoke(Generator, args)!; + } + + private X509Certificate2 CreateCertificateWithoutEku(string subject) + { + using RSA rsa = RSA.Create(2048); + CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + // Add basic constraints but no EKU + request.CertificateExtensions.Add( + new X509BasicConstraintsExtension(false, false, 0, true)); + + return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + } + + private X509Certificate2 CreateCertificateWithStandardEkus(string subject) + { + using RSA rsa = RSA.Create(2048); + CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + OidCollection oids = new() + { + new Oid("1.3.6.1.5.5.7.3.1"), // TLS Server auth + new Oid("1.3.6.1.5.5.7.3.2") // TLS Client auth + }; + + request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, false)); + + return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + } + + private X509Certificate2 CreateCertificateWithCustomEku(string subject, string ekuOid) + { + using RSA rsa = RSA.Create(2048); + CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + OidCollection oids = new() { new Oid(ekuOid) }; + request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, false)); + + return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + } + + private X509Certificate2 CreateCertificateWithMultipleEkus(string subject, List ekuOids) + { + using RSA rsa = RSA.Create(2048); + CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + OidCollection oids = new(); + foreach (string oid in ekuOids) + { + oids.Add(new Oid(oid)); + } + + request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, false)); + + return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + } + + private X509Certificate2 CreateSelfSignedCertificate(string subject) + { + using RSA rsa = RSA.Create(2048); + CertificateRequest request = new(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + // Mark as CA + request.CertificateExtensions.Add( + new X509BasicConstraintsExtension(true, false, 0, true)); + + return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + } + + #endregion +} diff --git a/CoseSign1.Certificates.AzureTrustedSigning.Tests/CoseSign1.Certificates.AzureTrustedSigning.Tests.csproj b/CoseSign1.Certificates.AzureArtifactSigning.Tests/CoseSign1.Certificates.AzureArtifactSigning.Tests.csproj similarity index 85% rename from CoseSign1.Certificates.AzureTrustedSigning.Tests/CoseSign1.Certificates.AzureTrustedSigning.Tests.csproj rename to CoseSign1.Certificates.AzureArtifactSigning.Tests/CoseSign1.Certificates.AzureArtifactSigning.Tests.csproj index 55618990..3f837cba 100644 --- a/CoseSign1.Certificates.AzureTrustedSigning.Tests/CoseSign1.Certificates.AzureTrustedSigning.Tests.csproj +++ b/CoseSign1.Certificates.AzureArtifactSigning.Tests/CoseSign1.Certificates.AzureArtifactSigning.Tests.csproj @@ -1,32 +1,32 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - - - - - - - - - + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs b/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs similarity index 76% rename from CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs rename to CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs index 32380104..77cab78f 100644 --- a/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs +++ b/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs @@ -1,129 +1,128 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace CoseSign1.Certificates.AzureTrustedSigning; - -using System; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography; -using Azure.Developer.TrustedSigning.CryptoProvider; -using CoseSign1.Certificates.Local; -using System.Linq; - -/// -/// Provides an implementation of the class -/// for Azure Trusted Signing. This class integrates with the Azure Trusted Signing service -/// to provide signing certificates, certificate chains, and cryptographic keys. -/// -public class AzureTrustedSigningCoseSigningKeyProvider : CertificateCoseSigningKeyProvider -{ - private readonly AzSignContext SignContext; - private static readonly AzureTrustedSigningDidX509Generator AzureDidGenerator = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The used to interact with Azure Trusted Signing. - /// Thrown if is null. - public AzureTrustedSigningCoseSigningKeyProvider(AzSignContext signContext) - { - SignContext = signContext ?? throw new ArgumentNullException(nameof(signContext)); - } - - /// - /// Gets the issuer value for CWT Claims, using Azure Trusted Signing specific DID:X509:0 format. - /// If non-standard EKUs are present, returns DID:X509:0 with EKU suffix, otherwise uses parent class behavior. - /// - /// - /// For Azure Trusted Signing certificates with non-standard EKUs, the format is: - /// did:x509:0:sha256:{rootHash}::eku:{deepestGreatestEku} - /// Otherwise, delegates to the base class implementation. - /// - public override string? Issuer - { - get - { - try - { - // Get the certificate chain in leaf-first order - IEnumerable certChain = GetCertificateChain(X509ChainSortOrder.LeafFirst); - - // Generate DID:X509:0 identifier from the chain using Azure-specific generator - return AzureDidGenerator.GenerateFromChain(certChain); - } - catch (Exception) - { - // If chain building or DID generation fails, fall back to base implementation - return base.Issuer; - } - } - } - - private readonly object CertificateChainLock = new object(); - private IReadOnlyList? CertificateChain; - - /// - /// Retrieves the certificate chain from the Azure Trusted Signing service. - /// - /// The desired sort order of the certificate chain (root-first or leaf-first). - /// An enumerable collection of objects representing the certificate chain. - /// - /// Thrown if the certificate chain is not available or is empty. - /// - protected override IEnumerable GetCertificateChain(X509ChainSortOrder sortOrder) - { - lock (CertificateChainLock) - { - CertificateChain ??= SignContext.GetCertChain() - ?? throw new InvalidOperationException("Certificate chain is not available. Please check the Azure Trusted Signing configuration."); - } - - X509Certificate2 firstCert = CertificateChain.FirstOrDefault() - ?? throw new InvalidOperationException("Certificate chain is empty. Please check the Azure Trusted Signing configuration."); - - // Determine if the certificate chain order needs to be reversed. - bool needsRevers = sortOrder == (firstCert.Issuer == firstCert.Subject ? X509ChainSortOrder.RootFirst : X509ChainSortOrder.LeafFirst); - - // Return the certificates in the specified order. - foreach (X509Certificate2 cert in needsRevers ? CertificateChain.Reverse() : CertificateChain) - { - yield return cert; - } - } - - private X509Certificate2? SigningCertificate; - - /// - /// Retrieves the signing certificate from the Azure Trusted Signing service. - /// - /// The object representing the signing certificate. - /// - /// Thrown if the signing certificate is not available. - /// - protected override X509Certificate2 GetSigningCertificate() - { - SigningCertificate ??= SignContext.GetSigningCertificate() - ?? throw new InvalidOperationException("Signing certificate is not available. Please check the Azure Trusted Signing configuration."); - return SigningCertificate; - } - - /// - /// Provides an ECDsa key for signing or verification operations. - /// - /// True to return the public key; false to return the private key (default). - /// Always throws a as ECDsa is not supported. - /// Thrown because ECDsa is not supported for Azure Trusted Signing. - protected override ECDsa? ProvideECDsaKey(bool publicKey = false) - => throw new NotSupportedException("ECDsa is not supported for Azure Trusted Signing CryptoProvider."); - - private RSAAzSign? RsaAzSignInstance; - - /// - /// Provides an RSA key for signing or verification operations. - /// - /// True to return the public key; false to return the private key (default). - /// An object representing the RSA key. - protected override RSA? ProvideRSAKey(bool publicKey = false) - => RsaAzSignInstance ??= new RSAAzSign(SignContext); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Certificates.AzureArtifactSigning; + +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using Azure.Developer.ArtifactSigning.CryptoProvider; +using CoseSign1.Certificates.Local; +using System.Linq; + +/// +/// Provides an implementation of the class +/// for Azure Artifact Signing. This class integrates with the Azure Artifact Signing service +/// to provide signing certificates, certificate chains, and cryptographic keys. +/// +public class AzureArtifactSigningCoseSigningKeyProvider : CertificateCoseSigningKeyProvider +{ + private readonly AzSignContext SignContext; + private static readonly AzureArtifactSigningDidX509Generator AzureDidGenerator = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The used to interact with Azure Artifact Signing. + /// Thrown if is null. + public AzureArtifactSigningCoseSigningKeyProvider(AzSignContext signContext) + { + SignContext = signContext ?? throw new ArgumentNullException(nameof(signContext)); + } + + /// + /// Gets the issuer value for CWT Claims, using Azure Artifact Signing specific DID:X509:0 format. + /// If non-standard EKUs are present, returns DID:X509:0 with EKU suffix, otherwise uses parent class behavior. + /// + /// + /// For Azure Artifact Signing certificates with non-standard EKUs, the format is: + /// did:x509:0:sha256:{rootHash}::eku:{deepestGreatestEku} + /// Otherwise, delegates to the base class implementation. + /// + public override string? Issuer + { + get + { + try + { + // Get the certificate chain in leaf-first order + IEnumerable certChain = GetCertificateChain(X509ChainSortOrder.LeafFirst); + + // Generate DID:X509:0 identifier from the chain using Azure-specific generator + return AzureDidGenerator.GenerateFromChain(certChain); + } + catch (Exception) + { + // If chain building or DID generation fails, fall back to base implementation + return base.Issuer; + } + } + } + + private readonly object CertificateChainLock = new object(); + private IReadOnlyList? CertificateChain; + + /// + /// Retrieves the certificate chain from the Azure Artifact Signing service. + /// + /// The desired sort order of the certificate chain (root-first or leaf-first). + /// An enumerable collection of objects representing the certificate chain. + /// + /// Thrown if the certificate chain is not available or is empty. + /// + protected override IEnumerable GetCertificateChain(X509ChainSortOrder sortOrder) + { + lock (CertificateChainLock) + { + CertificateChain ??= SignContext.GetCertChain() + ?? throw new InvalidOperationException("Certificate chain is not available. Please check the Azure Artifact Signing configuration."); + } + + X509Certificate2 firstCert = CertificateChain.FirstOrDefault() + ?? throw new InvalidOperationException("Certificate chain is empty. Please check the Azure Artifact Signing configuration."); + // Determine if the certificate chain order needs to be reversed. + bool needsRevers = sortOrder == (firstCert.Issuer == firstCert.Subject ? X509ChainSortOrder.RootFirst : X509ChainSortOrder.LeafFirst); + + // Return the certificates in the specified order. + foreach (X509Certificate2 cert in needsRevers ? CertificateChain.Reverse() : CertificateChain) + { + yield return cert; + } + } + + private X509Certificate2? SigningCertificate; + + /// + /// Retrieves the signing certificate from the Azure Artifact Signing service. + /// + /// The object representing the signing certificate. + /// + /// Thrown if the signing certificate is not available. + /// + protected override X509Certificate2 GetSigningCertificate() + { + SigningCertificate ??= SignContext.GetSigningCertificate() + ?? throw new InvalidOperationException("Signing certificate is not available. Please check the Azure Artifact Signing configuration."); + return SigningCertificate; + } + + /// + /// Provides an ECDsa key for signing or verification operations. + /// + /// True to return the public key; false to return the private key (default). + /// Always throws a as ECDsa is not supported. + /// Thrown because ECDsa is not supported for Azure Artifact Signing. + protected override ECDsa? ProvideECDsaKey(bool publicKey = false) + => throw new NotSupportedException("ECDsa is not supported for Azure Artifact Signing CryptoProvider."); + + private RSAAzSign? RsaAzSignInstance; + + /// + /// Provides an RSA key for signing or verification operations. + /// + /// True to return the public key; false to return the private key (default). + /// An object representing the RSA key. + protected override RSA? ProvideRSAKey(bool publicKey = false) + => RsaAzSignInstance ??= new RSAAzSign(SignContext); +} diff --git a/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs b/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningDidX509Generator.cs similarity index 88% rename from CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs rename to CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningDidX509Generator.cs index 29e0efa0..5f868ad8 100644 --- a/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs +++ b/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningDidX509Generator.cs @@ -1,218 +1,218 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace CoseSign1.Certificates.AzureTrustedSigning; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using CoseSign1.Certificates.Extensions; - -/// -/// Generates DID:X509:0 identifiers specifically for Azure Trusted Signing certificates. -/// Format: did:x509:0:sha256:{base64url-hash}::eku:{oid} -/// Per the DID:X509 specification at https://github.com/microsoft/did-x509/blob/main/specification.md#eku-policy -/// -/// -/// -/// Azure Trusted Signing certificates include Microsoft-specific Enhanced Key Usage (EKU) extensions -/// that identify the certificate's intended purpose. This generator creates EKU-based DID identifiers -/// when Microsoft EKUs (starting with 1.3.6.1.4.1.311) are present in the certificate. -/// -/// -/// Per the DID:X509 EKU Policy specification: -/// - Format: did:x509:0:{algorithm}:{base64url-hash}::eku:{oid} -/// - {oid} is a single OID from chain[0].extensions.eku in dotted decimal notation -/// - The OID is NOT percent-encoded (it's just the raw OID string) -/// - The base64url-encoded hash is 43 characters for SHA256 (RFC 4648 Section 5) -/// -/// -/// The "deepest greatest" Microsoft EKU is selected based on OID depth and last segment value -/// when multiple Microsoft EKUs are present. -/// -/// -public class AzureTrustedSigningDidX509Generator : DidX509Generator -{ - /// - /// Microsoft reserved EKU prefix used by Azure Trusted Signing certificates. - /// - private const string MicrosoftEkuPrefix = "1.3.6.1.4.1.311"; - - /// - /// Generates a DID:X509:0 identifier from an Azure Trusted Signing certificate chain. - /// Uses EKU-based format when Microsoft EKUs are present, otherwise delegates to base implementation. - /// - /// The certificate chain. First certificate must be the leaf. - /// - /// A DID:X509:0 formatted identifier. Example: - /// did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13 - /// - /// Thrown when certificates is null. - /// Thrown when chain is empty or invalid. - public override string GenerateFromChain(IEnumerable certificates) - { - if (certificates == null) - { - throw new ArgumentNullException(nameof(certificates)); - } - - X509Certificate2[] certArray = certificates.ToArray(); - - if (certArray.Length == 0) - { - throw new ArgumentException("Certificate chain cannot be empty.", nameof(certificates)); - } - - X509Certificate2 leafCert = certArray[0]; - - // Generate the base DID using subject policy from the base class - string baseDid = base.GenerateFromChain(certificates); - - // Extract EKUs from the leaf certificate - List ekus = ExtractEkus(leafCert); - - // Filter to Microsoft EKUs (Azure Trusted Signing specific) - List microsoftEkus = ekus - .Where(eku => eku.StartsWith(MicrosoftEkuPrefix, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - // If no Microsoft EKUs present, return the base implementation (subject policy) - if (microsoftEkus.Count == 0) - { - return baseDid; - } - - // Select the deepest greatest Microsoft EKU per Azure Trusted Signing conventions - string deepestGreatestEku = SelectDeepestGreatestEku(microsoftEkus); - - // Replace the ::subject: portion with ::eku:{oid} - // Per spec: policy-value for EKU is just the OID, not percent-encoded - int subjectIndex = baseDid.IndexOf("::subject:", StringComparison.OrdinalIgnoreCase); - if (subjectIndex == -1) - { - // Should not happen if base class follows spec, but handle gracefully - return baseDid; - } - - string didPrefix = baseDid.Substring(0, subjectIndex); - return $"{didPrefix}::eku:{deepestGreatestEku}"; - } - - /// - /// Extracts Enhanced Key Usage (EKU) OIDs from a certificate. - /// - /// The certificate to extract EKUs from. - /// A list of EKU OID strings in dotted decimal notation. - protected virtual List ExtractEkus(X509Certificate2 certificate) - { - List ekus = new(); - - // Find the Enhanced Key Usage extension - X509Extension? ekuExtension = certificate.Extensions - .OfType() - .FirstOrDefault(); - - if (ekuExtension == null) - { - return ekus; - } - - X509EnhancedKeyUsageExtension enhancedKeyUsage = (X509EnhancedKeyUsageExtension)ekuExtension; - - foreach (Oid oid in enhancedKeyUsage.EnhancedKeyUsages) - { - if (!string.IsNullOrEmpty(oid.Value)) - { - ekus.Add(oid.Value); - } - } - - return ekus; - } - - /// - /// Selects the "deepest greatest" Microsoft EKU from a list of EKU OIDs. - /// The deepest EKU is the one with the most OID segments (highest depth). - /// If multiple EKUs have the same depth, the one with the greatest last segment numeric value is selected. - /// - /// The list of Microsoft EKU OID strings to evaluate. - /// The OID string of the deepest greatest Microsoft EKU in dotted decimal notation. - protected virtual string SelectDeepestGreatestEku(List ekus) - { - if (ekus.Count == 0) - { - throw new ArgumentException("EKU list cannot be empty.", nameof(ekus)); - } - - if (ekus.Count == 1) - { - return ekus[0]; - } - - string deepestGreatest = ekus[0]; - int maxDepth = CountSegments(deepestGreatest); - long maxLastSegment = GetLastSegmentValue(deepestGreatest); - - for (int i = 1; i < ekus.Count; i++) - { - string currentEku = ekus[i]; - int currentDepth = CountSegments(currentEku); - long currentLastSegment = GetLastSegmentValue(currentEku); - - // Compare by depth first - if (currentDepth > maxDepth) - { - deepestGreatest = currentEku; - maxDepth = currentDepth; - maxLastSegment = currentLastSegment; - } - // If depth is the same, compare by last segment value - else if (currentDepth == maxDepth && currentLastSegment > maxLastSegment) - { - deepestGreatest = currentEku; - maxLastSegment = currentLastSegment; - } - } - - return deepestGreatest; - } - - /// - /// Counts the number of segments in an OID string (number of dots + 1). - /// - /// The OID string in dotted decimal notation. - /// The number of segments (depth of the OID). - protected virtual int CountSegments(string oid) - { - if (string.IsNullOrEmpty(oid)) - { - return 0; - } - - return oid.Split('.').Length; - } - - /// - /// Gets the numeric value of the last segment in an OID string. - /// - /// The OID string in dotted decimal notation. - /// The numeric value of the last segment, or 0 if parsing fails. - protected virtual long GetLastSegmentValue(string oid) - { - if (string.IsNullOrEmpty(oid)) - { - return 0; - } - - string[] segments = oid.Split('.'); - if (segments.Length == 0) - { - return 0; - } - - string lastSegment = segments[segments.Length - 1]; - return long.TryParse(lastSegment, out long value) ? value : 0; - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSign1.Certificates.AzureArtifactSigning; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using CoseSign1.Certificates.Extensions; + +/// +/// Generates DID:X509:0 identifiers specifically for Azure Artifact Signing certificates. +/// Format: did:x509:0:sha256:{base64url-hash}::eku:{oid} +/// Per the DID:X509 specification at https://github.com/microsoft/did-x509/blob/main/specification.md#eku-policy +/// +/// +/// +/// Azure Artifact Signing certificates include Microsoft-specific Enhanced Key Usage (EKU) extensions +/// that identify the certificate's intended purpose. This generator creates EKU-based DID identifiers +/// when Microsoft EKUs (starting with 1.3.6.1.4.1.311) are present in the certificate. +/// +/// +/// Per the DID:X509 EKU Policy specification: +/// - Format: did:x509:0:{algorithm}:{base64url-hash}::eku:{oid} +/// - {oid} is a single OID from chain[0].extensions.eku in dotted decimal notation +/// - The OID is NOT percent-encoded (it's just the raw OID string) +/// - The base64url-encoded hash is 43 characters for SHA256 (RFC 4648 Section 5) +/// +/// +/// The "deepest greatest" Microsoft EKU is selected based on OID depth and last segment value +/// when multiple Microsoft EKUs are present. +/// +/// +public class AzureArtifactSigningDidX509Generator : DidX509Generator +{ + /// + /// Microsoft reserved EKU prefix used by Azure Artifact Signing certificates. + /// + private const string MicrosoftEkuPrefix = "1.3.6.1.4.1.311"; + + /// + /// Generates a DID:X509:0 identifier from an Azure Artifact Signing certificate chain. + /// Uses EKU-based format when Microsoft EKUs are present, otherwise delegates to base implementation. + /// + /// The certificate chain. First certificate must be the leaf. + /// + /// A DID:X509:0 formatted identifier. Example: + /// did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13 + /// + /// Thrown when certificates is null. + /// Thrown when chain is empty or invalid. + public override string GenerateFromChain(IEnumerable certificates) + { + if (certificates == null) + { + throw new ArgumentNullException(nameof(certificates)); + } + + X509Certificate2[] certArray = certificates.ToArray(); + + if (certArray.Length == 0) + { + throw new ArgumentException("Certificate chain cannot be empty.", nameof(certificates)); + } + + X509Certificate2 leafCert = certArray[0]; + + // Generate the base DID using subject policy from the base class + string baseDid = base.GenerateFromChain(certificates); + + // Extract EKUs from the leaf certificate + List ekus = ExtractEkus(leafCert); + + // Filter to Microsoft EKUs (Azure Artifact Signing specific) + List microsoftEkus = ekus + .Where(eku => eku.StartsWith(MicrosoftEkuPrefix, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + // If no Microsoft EKUs present, return the base implementation (subject policy) + if (microsoftEkus.Count == 0) + { + return baseDid; + } + + // Select the deepest greatest Microsoft EKU per Azure Artifact Signing conventions + string deepestGreatestEku = SelectDeepestGreatestEku(microsoftEkus); + + // Replace the ::subject: portion with ::eku:{oid} + // Per spec: policy-value for EKU is just the OID, not percent-encoded + int subjectIndex = baseDid.IndexOf("::subject:", StringComparison.OrdinalIgnoreCase); + if (subjectIndex == -1) + { + // Should not happen if base class follows spec, but handle gracefully + return baseDid; + } + + string didPrefix = baseDid.Substring(0, subjectIndex); + return $"{didPrefix}::eku:{deepestGreatestEku}"; + } + + /// + /// Extracts Enhanced Key Usage (EKU) OIDs from a certificate. + /// + /// The certificate to extract EKUs from. + /// A list of EKU OID strings in dotted decimal notation. + protected virtual List ExtractEkus(X509Certificate2 certificate) + { + List ekus = new(); + + // Find the Enhanced Key Usage extension + X509Extension? ekuExtension = certificate.Extensions + .OfType() + .FirstOrDefault(); + + if (ekuExtension == null) + { + return ekus; + } + + X509EnhancedKeyUsageExtension enhancedKeyUsage = (X509EnhancedKeyUsageExtension)ekuExtension; + + foreach (Oid oid in enhancedKeyUsage.EnhancedKeyUsages) + { + if (!string.IsNullOrEmpty(oid.Value)) + { + ekus.Add(oid.Value); + } + } + + return ekus; + } + + /// + /// Selects the "deepest greatest" Microsoft EKU from a list of EKU OIDs. + /// The deepest EKU is the one with the most OID segments (highest depth). + /// If multiple EKUs have the same depth, the one with the greatest last segment numeric value is selected. + /// + /// The list of Microsoft EKU OID strings to evaluate. + /// The OID string of the deepest greatest Microsoft EKU in dotted decimal notation. + protected virtual string SelectDeepestGreatestEku(List ekus) + { + if (ekus.Count == 0) + { + throw new ArgumentException("EKU list cannot be empty.", nameof(ekus)); + } + + if (ekus.Count == 1) + { + return ekus[0]; + } + + string deepestGreatest = ekus[0]; + int maxDepth = CountSegments(deepestGreatest); + long maxLastSegment = GetLastSegmentValue(deepestGreatest); + + for (int i = 1; i < ekus.Count; i++) + { + string currentEku = ekus[i]; + int currentDepth = CountSegments(currentEku); + long currentLastSegment = GetLastSegmentValue(currentEku); + + // Compare by depth first + if (currentDepth > maxDepth) + { + deepestGreatest = currentEku; + maxDepth = currentDepth; + maxLastSegment = currentLastSegment; + } + // If depth is the same, compare by last segment value + else if (currentDepth == maxDepth && currentLastSegment > maxLastSegment) + { + deepestGreatest = currentEku; + maxLastSegment = currentLastSegment; + } + } + + return deepestGreatest; + } + + /// + /// Counts the number of segments in an OID string (number of dots + 1). + /// + /// The OID string in dotted decimal notation. + /// The number of segments (depth of the OID). + protected virtual int CountSegments(string oid) + { + if (string.IsNullOrEmpty(oid)) + { + return 0; + } + + return oid.Split('.').Length; + } + + /// + /// Gets the numeric value of the last segment in an OID string. + /// + /// The OID string in dotted decimal notation. + /// The numeric value of the last segment, or 0 if parsing fails. + protected virtual long GetLastSegmentValue(string oid) + { + if (string.IsNullOrEmpty(oid)) + { + return 0; + } + + string[] segments = oid.Split('.'); + if (segments.Length == 0) + { + return 0; + } + + string lastSegment = segments[segments.Length - 1]; + return long.TryParse(lastSegment, out long value) ? value : 0; + } +} diff --git a/CoseSign1.Certificates.AzureTrustedSigning/CoseSign1.Certificates.AzureTrustedSigning.csproj b/CoseSign1.Certificates.AzureArtifactSigning/CoseSign1.Certificates.AzureArtifactSigning.csproj similarity index 85% rename from CoseSign1.Certificates.AzureTrustedSigning/CoseSign1.Certificates.AzureTrustedSigning.csproj rename to CoseSign1.Certificates.AzureArtifactSigning/CoseSign1.Certificates.AzureArtifactSigning.csproj index 65ef4f0b..7808dc94 100644 --- a/CoseSign1.Certificates.AzureTrustedSigning/CoseSign1.Certificates.AzureTrustedSigning.csproj +++ b/CoseSign1.Certificates.AzureArtifactSigning/CoseSign1.Certificates.AzureArtifactSigning.csproj @@ -1,45 +1,45 @@ - - - - - net8.0 - latest - - - - - enable - true - true - latest - true - - - - - True - True - ..\StrongNameKeys\35MSSharedLib1024.snk - - - - - $(MsBuildProjectName) - $(VersionNgt) - Microsoft - LICENSE - false - readme.md - ChangeLog.md - Azure Trusted Signing implementation for CoseCign1.Certificate provider. - - - - - - - - - - - + + + + + net8.0 + latest + + + + + enable + true + true + latest + true + + + + + True + True + ..\StrongNameKeys\35MSSharedLib1024.snk + + + + + $(MsBuildProjectName) + $(VersionNgt) + Microsoft + LICENSE + false + readme.md + ChangeLog.md + Azure Artifact Signing implementation for CoseCign1.Certificate provider. + + + + + + + + + + + diff --git a/CoseSignTool.Abstractions/ICertificateProviderPlugin.cs b/CoseSignTool.Abstractions/ICertificateProviderPlugin.cs index 387500de..101d2941 100644 --- a/CoseSignTool.Abstractions/ICertificateProviderPlugin.cs +++ b/CoseSignTool.Abstractions/ICertificateProviderPlugin.cs @@ -32,11 +32,11 @@ public interface ICertificateProviderPlugin /// This name is used with the --cert-provider command-line parameter to select this provider. /// /// - /// The provider name should be lowercase, use hyphens for multiple words (e.g., "azure-trusted-signing"), + /// The provider name should be lowercase, use hyphens for multiple words (e.g., "azure-artifact-signing"), /// and be unique across all certificate provider plugins. /// /// - /// "local", "azure-trusted-signing", "aws-kms", "yubikey" + /// "local", "azure-artifact-signing", "aws-kms", "yubikey" /// string ProviderName { get; } @@ -53,7 +53,7 @@ public interface ICertificateProviderPlugin /// /// These options are merged into the Sign and indirect-sign commands when this provider is selected. /// Keys should be prefixed with a provider-specific identifier to avoid conflicts - /// (e.g., "--ats-endpoint" for Azure Trusted Signing). + /// (e.g., "--aas-endpoint" for Azure Artifact Signing). /// /// /// Security: Do NOT include options for raw tokens or secrets. Use credential mechanisms instead. @@ -61,7 +61,7 @@ public interface ICertificateProviderPlugin /// /// /// A dictionary mapping command-line switches to configuration keys. - /// Example: { "--ats-endpoint": "ats-endpoint", "--ats-account-name": "ats-account-name" } + /// Example: { "--aas-endpoint": "aas-endpoint", "--aas-account-name": "aas-account-name" } /// IDictionary GetProviderOptions(); diff --git a/CoseSignTool.AzureTrustedSigning.Plugin/AzureTrustedSigningCertificateProviderPlugin.cs b/CoseSignTool.AzureArtifactSigning.Plugin/AzureArtifactSigningCertificateProviderPlugin.cs similarity index 61% rename from CoseSignTool.AzureTrustedSigning.Plugin/AzureTrustedSigningCertificateProviderPlugin.cs rename to CoseSignTool.AzureArtifactSigning.Plugin/AzureArtifactSigningCertificateProviderPlugin.cs index 2be96851..583c5df6 100644 --- a/CoseSignTool.AzureTrustedSigning.Plugin/AzureTrustedSigningCertificateProviderPlugin.cs +++ b/CoseSignTool.AzureArtifactSigning.Plugin/AzureArtifactSigningCertificateProviderPlugin.cs @@ -1,218 +1,218 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace CoseSignTool.AzureTrustedSigning.Plugin; - -using Azure.Core; -using Azure.Developer.TrustedSigning.CryptoProvider; -using Azure.Identity; -using CoseSign1.Certificates.AzureTrustedSigning; -using System; - -/// -/// Certificate provider plugin for Azure Trusted Signing service. -/// Enables CoseSignTool to use Azure Trusted Signing for certificate-based COSE signing operations. -/// -/// -/// -/// This plugin integrates Azure Trusted Signing into CoseSignTool's Sign and indirect-sign commands. -/// It uses DefaultAzureCredential for secure authentication, supporting managed identities, -/// Azure CLI credentials, environment variables, and other Azure SDK authentication mechanisms. -/// -/// -/// Security: This plugin NEVER accepts raw tokens on the command line. All authentication -/// is handled through DefaultAzureCredential, which uses secure, industry-standard -/// credential acquisition methods. -/// -/// -public class AzureTrustedSigningCertificateProviderPlugin : ICertificateProviderPlugin -{ - /// - public string ProviderName => "azure-trusted-signing"; - - /// - public string Description => "Azure Trusted Signing cloud-based certificate provider"; - - /// - public IDictionary GetProviderOptions() - { - return new Dictionary - { - ["--ats-endpoint"] = "ats-endpoint", - ["--ats-account-name"] = "ats-account-name", - ["--ats-cert-profile-name"] = "ats-cert-profile-name", - }; - } - - /// - public bool CanCreateProvider(IConfiguration configuration) - { - // Check for required parameters - string? endpoint = configuration["ats-endpoint"]; - string? accountName = configuration["ats-account-name"]; - string? certProfileName = configuration["ats-cert-profile-name"]; - - return !string.IsNullOrWhiteSpace(endpoint) && - !string.IsNullOrWhiteSpace(accountName) && - !string.IsNullOrWhiteSpace(certProfileName); - } - - /// - public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null) - { - // Extract required parameters - string? endpoint = configuration["ats-endpoint"]; - string? accountName = configuration["ats-account-name"]; - string? certProfileName = configuration["ats-cert-profile-name"]; - - // Validate required parameters - if (string.IsNullOrWhiteSpace(endpoint)) - { - throw new ArgumentException("Azure Trusted Signing endpoint (--ats-endpoint) is required.", nameof(configuration)); - } - - if (string.IsNullOrWhiteSpace(accountName)) - { - throw new ArgumentException("Azure Trusted Signing account name (--ats-account-name) is required.", nameof(configuration)); - } - - if (string.IsNullOrWhiteSpace(certProfileName)) - { - throw new ArgumentException("Azure Trusted Signing certificate profile name (--ats-cert-profile-name) is required.", nameof(configuration)); - } - - try - { - logger?.LogVerbose($"Creating Azure Trusted Signing provider..."); - logger?.LogVerbose($" Endpoint: {endpoint}"); - logger?.LogVerbose($" Account: {accountName}"); - logger?.LogVerbose($" Certificate Profile: {certProfileName}"); - - // Create Azure credential using DefaultAzureCredential - // This supports multiple authentication methods in order of precedence: - // 1. Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, etc.) - // 2. Managed Identity (for Azure VMs, App Service, etc.) - // 3. Visual Studio credential - // 4. Azure CLI credential - // 5. Azure PowerShell credential - logger?.LogVerbose("Acquiring Azure credentials using DefaultAzureCredential..."); - TokenCredential credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions // CodeQL [SM02196] DefaultAzureCredential is the recommended approach for client applications and libraries to authenticate to Azure services - { - // Exclude interactive browser auth to avoid unexpected prompts in CI/CD - ExcludeInteractiveBrowserCredential = true - }); - - logger?.LogVerbose("Creating CertificateProfileClient..."); - // Create the Certificate Profile Client - // Constructor: CertificateProfileClient(TokenCredential credential, Uri endpoint, options) - Uri endpointUri = new Uri(endpoint); - var certificateProfileClient = new Azure.CodeSigning.CertificateProfileClient( - credential, - endpointUri); - - logger?.LogVerbose("Creating AzSignContext..."); - // Create AzSignContext using the certificate profile client - AzSignContext signContext = new AzSignContext( - endpoint, - accountName, - certificateProfileClient); - - logger?.LogVerbose("Creating AzureTrustedSigningCoseSigningKeyProvider..."); - // Create and return the key provider - AzureTrustedSigningCoseSigningKeyProvider provider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); - - logger?.LogInformation("Azure Trusted Signing provider created successfully."); - return provider; - } - catch (ArgumentException) - { - // Re-throw argument exceptions as-is - throw; - } - catch (UriFormatException ex) - { - logger?.LogError($"Invalid Azure Trusted Signing endpoint URL: {endpoint}"); - throw new ArgumentException($"Invalid Azure Trusted Signing endpoint URL: {endpoint}. Ensure it is a valid HTTPS URL.", nameof(configuration), ex); - } - catch (Exception ex) - { - logger?.LogError($"Failed to create Azure Trusted Signing provider: {ex.Message}"); - logger?.LogException(ex); - throw new InvalidOperationException( - "Failed to create Azure Trusted Signing provider. " + - "Ensure Azure credentials are properly configured (environment variables, managed identity, Azure CLI, etc.) " + - "and the specified endpoint, account name, and certificate profile are correct.", - ex); - } - } - - /// - public string GetUsageDocumentation() - { - return @" -Azure Trusted Signing Certificate Provider -========================================== - -The Azure Trusted Signing provider enables signing with certificates managed by Azure Trusted Signing, -a cloud-based certificate management and signing service. - -Required Parameters: - --ats-endpoint Azure Trusted Signing endpoint URL - Example: https://myaccount.codesigning.azure.net - - --ats-account-name Azure Trusted Signing account name - Example: MySigningAccount - - --ats-cert-profile-name Certificate profile name within the account - Example: MyCodeSigningProfile - -Authentication: - This provider uses DefaultAzureCredential for authentication, which supports: - - Managed Identity (Azure VMs, App Service, Container Instances, etc.) - - Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, etc.) - - Azure CLI (az login) - - Azure PowerShell (Connect-AzAccount) - - Visual Studio credential - - For CI/CD scenarios, configure environment variables or use managed identity. - For local development, use 'az login' or Visual Studio authentication. - - Security Note: This provider NEVER accepts raw tokens or secrets on the command line. - All authentication uses secure Azure SDK credential mechanisms. - -Examples: - # Sign with Azure Trusted Signing (using Azure CLI credentials) - az login - CoseSignTool sign --payload file.bin --signature file.cose \ - --cert-provider azure-trusted-signing \ - --ats-endpoint https://myaccount.codesigning.azure.net \ - --ats-account-name MySigningAccount \ - --ats-cert-profile-name MyCodeSigningProfile - - # Indirect sign with Azure Trusted Signing (using managed identity in Azure) - CoseSignTool indirect-sign --payload file.bin --signature file.cose \ - --cert-provider azure-trusted-signing \ - --ats-endpoint https://myaccount.codesigning.azure.net \ - --ats-account-name MySigningAccount \ - --ats-cert-profile-name MyCodeSigningProfile - - # Using environment variables for authentication (CI/CD) - export AZURE_TENANT_ID=your-tenant-id - export AZURE_CLIENT_ID=your-client-id - export AZURE_CLIENT_SECRET=your-client-secret - CoseSignTool sign --payload file.bin --signature file.cose \ - --cert-provider azure-trusted-signing \ - --ats-endpoint https://myaccount.codesigning.azure.net \ - --ats-account-name MySigningAccount \ - --ats-cert-profile-name MyCodeSigningProfile - -Troubleshooting: - - Ensure you have proper Azure credentials configured - - Verify the endpoint URL is correct and accessible - - Confirm your Azure identity has appropriate permissions for the signing account - - Check that the account name and certificate profile name exist - - For managed identity issues, verify the identity is enabled and has required roles - - For Azure CLI issues, try running 'az login' again -"; - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CoseSignTool.AzureArtifactSigning.Plugin; + +using Azure.Core; +using Azure.Developer.ArtifactSigning.CryptoProvider; +using Azure.Identity; +using CoseSign1.Certificates.AzureArtifactSigning; +using System; + +/// +/// Certificate provider plugin for Azure Artifact Signing service. +/// Enables CoseSignTool to use Azure Artifact Signing for certificate-based COSE signing operations. +/// +/// +/// +/// This plugin integrates Azure Artifact Signing into CoseSignTool's Sign and indirect-sign commands. +/// It uses DefaultAzureCredential for secure authentication, supporting managed identities, +/// Azure CLI credentials, environment variables, and other Azure SDK authentication mechanisms. +/// +/// +/// Security: This plugin NEVER accepts raw tokens on the command line. All authentication +/// is handled through DefaultAzureCredential, which uses secure, industry-standard +/// credential acquisition methods. +/// +/// +public class AzureArtifactSigningCertificateProviderPlugin : ICertificateProviderPlugin +{ + /// + public string ProviderName => "azure-artifact-signing"; + + /// + public string Description => "Azure Artifact Signing cloud-based certificate provider"; + + /// + public IDictionary GetProviderOptions() + { + return new Dictionary + { + ["--aas-endpoint"] = "aas-endpoint", + ["--aas-account-name"] = "aas-account-name", + ["--aas-cert-profile-name"] = "aas-cert-profile-name", + }; + } + + /// + public bool CanCreateProvider(IConfiguration configuration) + { + // Check for required parameters + string? endpoint = configuration["aas-endpoint"]; + string? accountName = configuration["aas-account-name"]; + string? certProfileName = configuration["aas-cert-profile-name"]; + + return !string.IsNullOrWhiteSpace(endpoint) && + !string.IsNullOrWhiteSpace(accountName) && + !string.IsNullOrWhiteSpace(certProfileName); + } + + /// + public ICoseSigningKeyProvider CreateProvider(IConfiguration configuration, IPluginLogger? logger = null) + { + // Extract required parameters + string? endpoint = configuration["aas-endpoint"]; + string? accountName = configuration["aas-account-name"]; + string? certProfileName = configuration["aas-cert-profile-name"]; + + // Validate required parameters + if (string.IsNullOrWhiteSpace(endpoint)) + { + throw new ArgumentException("Azure Artifact Signing endpoint (--aas-endpoint) is required.", nameof(configuration)); + } + + if (string.IsNullOrWhiteSpace(accountName)) + { + throw new ArgumentException("Azure Artifact Signing account name (--aas-account-name) is required.", nameof(configuration)); + } + + if (string.IsNullOrWhiteSpace(certProfileName)) + { + throw new ArgumentException("Azure Artifact Signing certificate profile name (--aas-cert-profile-name) is required.", nameof(configuration)); + } + + try + { + logger?.LogVerbose($"Creating Azure Artifact Signing provider..."); + logger?.LogVerbose($" Endpoint: {endpoint}"); + logger?.LogVerbose($" Account: {accountName}"); + logger?.LogVerbose($" Certificate Profile: {certProfileName}"); + + // Create Azure credential using DefaultAzureCredential + // This supports multiple authentication methods in order of precedence: + // 1. Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, etc.) + // 2. Managed Identity (for Azure VMs, App Service, etc.) + // 3. Visual Studio credential + // 4. Azure CLI credential + // 5. Azure PowerShell credential + logger?.LogVerbose("Acquiring Azure credentials using DefaultAzureCredential..."); + TokenCredential credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions // CodeQL [SM02196] DefaultAzureCredential is the recommended approach for client applications and libraries to authenticate to Azure services + { + // Exclude interactive browser auth to avoid unexpected prompts in CI/CD + ExcludeInteractiveBrowserCredential = true + }); + + logger?.LogVerbose("Creating CertificateProfileClient..."); + // Create the Certificate Profile Client + // Constructor: CertificateProfileClient(TokenCredential credential, Uri endpoint, options) + Uri endpointUri = new Uri(endpoint); + var certificateProfileClient = new Azure.CodeSigning.CertificateProfileClient( + credential, + endpointUri); + + logger?.LogVerbose("Creating AzSignContext..."); + // Create AzSignContext using the certificate profile client + AzSignContext signContext = new AzSignContext( + endpoint, + accountName, + certificateProfileClient); + + logger?.LogVerbose("Creating AzureArtifactSigningCoseSigningKeyProvider..."); + // Create and return the key provider + AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(signContext); + + logger?.LogInformation("Azure Artifact Signing provider created successfully."); + return provider; + } + catch (ArgumentException) + { + // Re-throw argument exceptions as-is + throw; + } + catch (UriFormatException ex) + { + logger?.LogError($"Invalid Azure Artifact Signing endpoint URL: {endpoint}"); + throw new ArgumentException($"Invalid Azure Artifact Signing endpoint URL: {endpoint}. Ensure it is a valid HTTPS URL.", nameof(configuration), ex); + } + catch (Exception ex) + { + logger?.LogError($"Failed to create Azure Artifact Signing provider: {ex.Message}"); + logger?.LogException(ex); + throw new InvalidOperationException( + "Failed to create Azure Artifact Signing provider. " + + "Ensure Azure credentials are properly configured (environment variables, managed identity, Azure CLI, etc.) " + + "and the specified endpoint, account name, and certificate profile are correct.", + ex); + } + } + + /// + public string GetUsageDocumentation() + { + return @" +Azure Artifact Signing Certificate Provider +========================================== + +The Azure Artifact Signing provider enables signing with certificates managed by Azure Artifact Signing, +a cloud-based certificate management and signing service. + +Required Parameters: + --aas-endpoint Azure Artifact Signing endpoint URL + Example: https://myaccount.codesigning.azure.net + + --aas-account-name Azure Artifact Signing account name + Example: MySigningAccount + + --aas-cert-profile-name Certificate profile name within the account + Example: MyCodeSigningProfile + +Authentication: + This provider uses DefaultAzureCredential for authentication, which supports: + - Managed Identity (Azure VMs, App Service, Container Instances, etc.) + - Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, etc.) + - Azure CLI (az login) + - Azure PowerShell (Connect-AzAccount) + - Visual Studio credential + + For CI/CD scenarios, configure environment variables or use managed identity. + For local development, use 'az login' or Visual Studio authentication. + + Security Note: This provider NEVER accepts raw tokens or secrets on the command line. + All authentication uses secure Azure SDK credential mechanisms. + +Examples: + # Sign with Azure Artifact Signing (using Azure CLI credentials) + az login + CoseSignTool sign --payload file.bin --signature file.cose \ + --cert-provider azure-artifact-signing \ + --aas-endpoint https://myaccount.codesigning.azure.net \ + --aas-account-name MySigningAccount \ + --aas-cert-profile-name MyCodeSigningProfile + + # Indirect sign with Azure Artifact Signing (using managed identity in Azure) + CoseSignTool indirect-sign --payload file.bin --signature file.cose \ + --cert-provider azure-artifact-signing \ + --aas-endpoint https://myaccount.codesigning.azure.net \ + --aas-account-name MySigningAccount \ + --aas-cert-profile-name MyCodeSigningProfile + + # Using environment variables for authentication (CI/CD) + export AZURE_TENANT_ID=your-tenant-id + export AZURE_CLIENT_ID=your-client-id + export AZURE_CLIENT_SECRET=your-client-secret + CoseSignTool sign --payload file.bin --signature file.cose \ + --cert-provider azure-artifact-signing \ + --aas-endpoint https://myaccount.codesigning.azure.net \ + --aas-account-name MySigningAccount \ + --aas-cert-profile-name MyCodeSigningProfile + +Troubleshooting: + - Ensure you have proper Azure credentials configured + - Verify the endpoint URL is correct and accessible + - Confirm your Azure identity has appropriate permissions for the signing account + - Check that the account name and certificate profile name exist + - For managed identity issues, verify the identity is enabled and has required roles + - For Azure CLI issues, try running 'az login' again +"; + } +} diff --git a/CoseSignTool.AzureTrustedSigning.Plugin/CoseSignTool.AzureTrustedSigning.Plugin.csproj b/CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj similarity index 100% rename from CoseSignTool.AzureTrustedSigning.Plugin/CoseSignTool.AzureTrustedSigning.Plugin.csproj rename to CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj diff --git a/CoseSignTool.AzureTrustedSigning.Plugin/Usings.cs b/CoseSignTool.AzureArtifactSigning.Plugin/Usings.cs similarity index 97% rename from CoseSignTool.AzureTrustedSigning.Plugin/Usings.cs rename to CoseSignTool.AzureArtifactSigning.Plugin/Usings.cs index 63ecc702..09f93e5a 100644 --- a/CoseSignTool.AzureTrustedSigning.Plugin/Usings.cs +++ b/CoseSignTool.AzureArtifactSigning.Plugin/Usings.cs @@ -1,6 +1,6 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -global using Microsoft.Extensions.Configuration; -global using CoseSignTool.Abstractions; -global using CoseSign1.Abstractions.Interfaces; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using Microsoft.Extensions.Configuration; +global using CoseSignTool.Abstractions; +global using CoseSign1.Abstractions.Interfaces; diff --git a/CoseSignTool.IndirectSignature.Plugin/IndirectSignCommand.cs b/CoseSignTool.IndirectSignature.Plugin/IndirectSignCommand.cs index 3ed727e0..ea336a2c 100644 --- a/CoseSignTool.IndirectSignature.Plugin/IndirectSignCommand.cs +++ b/CoseSignTool.IndirectSignature.Plugin/IndirectSignCommand.cs @@ -31,24 +31,24 @@ public override IDictionary Options { options[option.Key] = option.Value; } - + // Add CWT Claims options for SCITT compliance options["enable-scitt"] = "Enable SCITT compliance with automatic CWT claims (default: true)"; options["cwt-issuer"] = "The CWT issuer (iss) claim. Defaults to DID:x509 identity from certificate"; options["cwt-subject"] = "The CWT subject (sub) claim. Defaults to 'UnknownIntent'"; options["cwt-audience"] = "The CWT audience (aud) claim (optional)"; - + // Add payload location option for CoseHashEnvelope format options["payload-location"] = "A URI indicating where the payload can be retrieved from (optional, CoseHashEnvelope format only)"; - + return options; } } /// - public override string Usage => GetBaseUsage("indirect-sign", "sign") + - GetCertificateUsage() + - GetAdditionalOptionalArguments() + + public override string Usage => GetBaseUsage("indirect-sign", "sign") + + GetCertificateUsage() + + GetAdditionalOptionalArguments() + GetCertificateProviderInfo() + GetExamples(); @@ -112,7 +112,7 @@ public override async Task ExecuteAsync(IConfiguration configura // Parse algorithm and version parameters HashAlgorithmName hashAlgorithm; IndirectSignatureFactory.IndirectSignatureVersion signatureVersion; - + try { hashAlgorithm = ParseHashAlgorithm(configuration); @@ -129,7 +129,7 @@ public override async Task ExecuteAsync(IConfiguration configura string? cwtIssuer = GetOptionalValue(configuration, "cwt-issuer"); string? cwtSubject = GetOptionalValue(configuration, "cwt-subject"); string? cwtAudience = GetOptionalValue(configuration, "cwt-audience"); - + // Parse custom CWT claims (can be specified multiple times) List? cwtClaims = null; string? cwtClaimsValue = GetOptionalValue(configuration, "cwt-claims"); @@ -149,7 +149,7 @@ public override async Task ExecuteAsync(IConfiguration configura index++; } } - + Logger.LogVerbose($"SCITT compliance: {enableScitt}"); if (!string.IsNullOrEmpty(cwtIssuer)) { @@ -179,12 +179,12 @@ public override async Task ExecuteAsync(IConfiguration configura using CancellationTokenSource combinedCts = CreateTimeoutCancellationToken(timeoutSeconds, cancellationToken); Logger.LogVerbose($"Creating indirect signature with hash algorithm: {hashAlgorithm.Name}, version: {signatureVersion}"); (PluginExitCode exitCode, JsonElement? result) = await CreateIndirectSignature( - payloadPath, - signaturePath, - certificate, + payloadPath, + signaturePath, + certificate, additionalCertificates, - contentType, - hashAlgorithm, + contentType, + hashAlgorithm, signatureVersion, configuration, enableScitt, @@ -264,7 +264,7 @@ public override async Task ExecuteAsync(IConfiguration configura logger.LogVerbose($"Reading payload from: {payloadPath}"); byte[] payload = await File.ReadAllBytesAsync(payloadPath, cancellationToken); logger.LogVerbose($"Payload size: {payload.Length} bytes"); - + // Create signing key provider logger.LogVerbose($"Using certificate: {certificate.Subject}"); logger.LogVerbose($"Certificate thumbprint: {certificate.Thumbprint}"); @@ -287,7 +287,7 @@ public override async Task ExecuteAsync(IConfiguration configura if (!string.IsNullOrEmpty(cwtIssuer) || !string.IsNullOrEmpty(cwtSubject) || !string.IsNullOrEmpty(cwtAudience) || (cwtClaims != null && cwtClaims.Count > 0)) { logger.LogVerbose("Creating CWT claims customizer to override defaults"); - + // Create a CWT claims extender with user-specified values // This will merge with and override the automatic defaults from CertificateCoseSigningKeyProvider CoseSign1.Headers.CWTClaimsHeaderExtender cwtCustomizer = new(); @@ -334,7 +334,7 @@ public override async Task ExecuteAsync(IConfiguration configura // Create indirect signature factory using IndirectSignatureFactory factory = new IndirectSignatureFactory(hashAlgorithm); - + // Create the indirect signature with optional header extender // Note: When EnableScittCompliance is true, CertificateCoseSigningKeyProvider automatically includes default CWT claims logger.LogVerbose("Creating indirect signature..."); @@ -513,10 +513,10 @@ private static string GetCertificateUsage() usage.AppendLine(); usage.AppendLine("Certificate options (one source required for signing):"); usage.AppendLine(); - + // Add certificate provider options if any are available usage.AppendLine(" Certificate Provider Plugin (recommended for cloud/HSM signing):"); - usage.AppendLine(" --cert-provider Use a certificate provider plugin (e.g., azure-trusted-signing)"); + usage.AppendLine(" --cert-provider Use a certificate provider plugin (e.g., azure-artifact-signing)"); usage.AppendLine(" See Certificate Providers section below for available providers"); usage.AppendLine(); usage.AppendLine(" --OR--"); @@ -541,7 +541,7 @@ private static string GetCertificateUsage() usage.AppendLine(" Labels: integers or RFC 8392 names (iss, sub, aud, exp, nbf, iat, cti)."); usage.AppendLine(" Timestamps accept date/time strings or Unix timestamps."); usage.AppendLine(" Examples: --cwt-claims \"exp:2024-12-31T23:59:59Z\" --cwt-claims \"100:custom-value\""); - + return usage.ToString(); } @@ -561,7 +561,7 @@ private static string GetCertificateProviderInfo() System.Reflection.FieldInfo? managerField = coseSignToolType.GetField( "CertificateProviderManager", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); - + if (managerField == null) { return string.Empty; @@ -578,11 +578,11 @@ private static string GetCertificateProviderInfo() sb.AppendLine(); sb.AppendLine(" The following certificate provider plugins are available:"); sb.AppendLine(); - + foreach (var kvp in manager.Providers) { sb.AppendLine($" {kvp.Key,-30} {kvp.Value.Description}"); - + // Show required parameters if available var providerOptions = kvp.Value.GetProviderOptions(); if (providerOptions.Any()) @@ -596,10 +596,10 @@ private static string GetCertificateProviderInfo() } sb.AppendLine(); } - + sb.AppendLine(" For detailed documentation, use: CoseSignTool help "); sb.AppendLine(); - + return sb.ToString(); } diff --git a/CoseSignTool.sln b/CoseSignTool.sln index a37d4c6d..43a8ba50 100644 --- a/CoseSignTool.sln +++ b/CoseSignTool.sln @@ -39,7 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{36BAA2CE-A docs\CoseHandler.md = docs\CoseHandler.md docs\CoseIndirectSignature.md = docs\CoseIndirectSignature.md docs\CoseSign1.Abstractions.md = docs\CoseSign1.Abstractions.md - docs\CoseSign1.Certificates.AzureTrustedSigning.md = docs\CoseSign1.Certificates.AzureTrustedSigning.md + docs\CoseSign1.Certificates.AzureArtifactSigning.md = docs\CoseSign1.Certificates.AzureArtifactSigning.md docs\CoseSign1.Certificates.md = docs\CoseSign1.Certificates.md CoseSign1.md = CoseSign1.md docs\CoseSign1.Transparent.md = docs\CoseSign1.Transparent.md @@ -69,9 +69,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseSign1.Headers", "CoseSi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoseSign1.Headers.Tests", "CoseSign1.Headers.Tests\CoseSign1.Headers.Tests.csproj", "{5181310A-CA82-4399-9197-86B468F7FCE9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Certificates.AzureTrustedSigning", "CoseSign1.Certificates.AzureTrustedSigning\CoseSign1.Certificates.AzureTrustedSigning.csproj", "{F2AA73A9-E5AD-431A-83E7-4BCEFD5C7CCD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Certificates.AzureArtifactSigning", "CoseSign1.Certificates.AzureArtifactSigning\CoseSign1.Certificates.AzureArtifactSigning.csproj", "{F2AA73A9-E5AD-431A-83E7-4BCEFD5C7CCD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Certificates.AzureTrustedSigning.Tests", "CoseSign1.Certificates.AzureTrustedSigning.Tests\CoseSign1.Certificates.AzureTrustedSigning.Tests.csproj", "{14C76799-55FD-4A17-AB97-5148912A2CB2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Certificates.AzureArtifactSigning.Tests", "CoseSign1.Certificates.AzureArtifactSigning.Tests\CoseSign1.Certificates.AzureArtifactSigning.Tests.csproj", "{14C76799-55FD-4A17-AB97-5148912A2CB2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSign1.Transparent", "CoseSign1.Transparent\CoseSign1.Transparent.csproj", "{F01E36FD-EF26-4F61-B025-1E9C603047D3}" EndProject @@ -93,7 +93,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSignTool.IndirectSignat EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSignTool.IndirectSignature.Plugin.Tests", "CoseSignTool.IndirectSignature.Plugin.Tests\CoseSignTool.IndirectSignature.Plugin.Tests.csproj", "{B8D3E1F2-4C6A-9E7B-A5F3-8D2C1E9A6B4D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSignTool.AzureTrustedSigning.Plugin", "CoseSignTool.AzureTrustedSigning.Plugin\CoseSignTool.AzureTrustedSigning.Plugin.csproj", "{D41AF01F-9174-4F29-A030-8BA2FF43E8D2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoseSignTool.AzureArtifactSigning.Plugin", "CoseSignTool.AzureArtifactSigning.Plugin\CoseSignTool.AzureArtifactSigning.Plugin.csproj", "{D41AF01F-9174-4F29-A030-8BA2FF43E8D2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Core.TestCommon", "Azure.Core.TestCommon\Azure.Core.TestCommon.csproj", "{6EB58A79-D799-4038-BF97-027F50CE69BD}" EndProject diff --git a/docs/CoseSign1.Certificates.AzureTrustedSigning.md b/docs/CoseSign1.Certificates.AzureArtifactSigning.md similarity index 78% rename from docs/CoseSign1.Certificates.AzureTrustedSigning.md rename to docs/CoseSign1.Certificates.AzureArtifactSigning.md index 0a5e8c16..743d0138 100644 --- a/docs/CoseSign1.Certificates.AzureTrustedSigning.md +++ b/docs/CoseSign1.Certificates.AzureArtifactSigning.md @@ -1,240 +1,240 @@ -# [CoseSign1.Certificates.AzureTrustedSigning](https://github.com/microsoft/CoseSignTool/tree/main/CoseSign1.Certificates.AzureTrustedSigning) - -**CoseSign1.Certificates.AzureTrustedSigning** is a .NET 8.0 library that provides an implementation of the `CertificateCoseSigningKeyProvider` class for Azure Trusted Signing. This library integrates with the Azure Trusted Signing service to provide signing certificates, certificate chains, and cryptographic keys. - -## Requirements -- **.NET 8.0 Runtime**: This library requires .NET 8.0 or later. -- **Azure Trusted Signing Account**: An active Azure Trusted Signing account with configured certificate profiles. -- **Azure Authentication**: Valid Azure credentials (Azure CLI, Managed Identity, environment variables, etc.). - -## Dependencies -**CoseSign1.Certificates.AzureTrustedSigning** has the following package dependencies: -* **CoseSign1.Certificates** - Base certificate provider abstractions -* **Azure.Developer.TrustedSigning.CryptoProvider** - Azure Trusted Signing SDK for cryptographic operations -* **Azure.Identity** (transitive) - Azure authentication mechanisms -* **Azure.Core** (transitive) - Azure SDK core functionality - -## Creation -The following class is provided for creating a proper `CoseSign1Message` object signed by an Azure Trusted Signing certificate. - -### [AzureTrustedSigningCoseSigningKeyProvider](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs) -This class is a concrete implementation of `CertificateCoseSigningKeyProvider` that integrates with the Azure Trusted Signing service. It provides the following functionality: -* Retrieves the signing certificate from the Azure Trusted Signing service. -* Retrieves the certificate chain from the Azure Trusted Signing service. -* Provides an RSA key for signing or verification operations. - -#### Key Features: -- **GetSigningCertificate**: Retrieves the signing certificate from the Azure Trusted Signing service. -- **GetCertificateChain**: Retrieves the certificate chain in the desired sort order (root-first or leaf-first). -- **ProvideRSAKey**: Provides an RSA key for signing or verification operations. -- **ProvideECDsaKey**: Not supported for Azure Trusted Signing and will throw a `NotSupportedException`. -- **Issuer Property (SCITT)**: Overrides the base class to provide Azure-specific DID:X509:0 format with EKU support for non-standard certificates. - -#### Constructor: -- `AzureTrustedSigningCoseSigningKeyProvider(AzSignContext signContext)`: Initializes a new instance of the class using the provided `AzSignContext`. -#### Exceptions: -- `ArgumentNullException`: Thrown if the `signContext` parameter is null. -- `InvalidOperationException`: Thrown if the signing certificate or certificate chain is not available or is empty. -- `NotSupportedException`: Thrown when attempting to use ECDsa keys, as they are not supported. - -## SCITT Compliance and DID:X509:0 Support - -Azure Trusted Signing certificates automatically support **SCITT (Supply Chain Integrity, Transparency, and Trust)** compliance through an enhanced DID:X509:0 format that includes Extended Key Usage (EKU) information per the [DID:X509 specification](https://github.com/microsoft/did-x509/blob/main/specification.md#eku-policy). - -### DID:X509:0 Format with EKU - -Azure Trusted Signing certificates include **Microsoft-specific EKUs** (Enhanced Key Usages starting with `1.3.6.1.4.1.311`) that identify the certificate's intended purpose. When these Microsoft EKUs are present, the `Issuer` property automatically generates an EKU-based DID identifier: - -``` -did:x509:0:sha256:{base64url-hash}::eku:{oid} -``` - -Where: -- `{base64url-hash}` is the base64url-encoded root certificate fingerprint (43 characters for SHA256 per RFC 4648 Section 5) -- `{oid}` is the EKU OID in dotted decimal notation (e.g., `1.3.6.1.4.1.311.10.3.13`) -- The OID is NOT percent-encoded (just the raw OID string) - -#### Microsoft EKU Detection -The generator detects any EKU starting with the **Microsoft reserved prefix `1.3.6.1.4.1.311`**. When Microsoft EKUs are present, an EKU-based DID is generated. When no Microsoft EKUs are found, the generator falls back to the standard subject-based DID format. - -#### Deepest Greatest EKU Selection -When multiple Microsoft EKUs are present in an Azure Trusted Signing certificate, the "deepest greatest" EKU is selected using: -1. **Depth Priority**: EKU with the most OID segments (e.g., `1.3.6.1.4.1.311.20.30` has 9 segments) -2. **Last Segment Tiebreaker**: If multiple EKUs have the same depth, the one with the largest last segment value is selected - -**Examples:** -- `1.3.6.1.4.1.311.20.99` beats `1.3.6.1.4.1.311.20.50` (same depth, 99 > 50) -- `1.3.6.1.4.1.311.20.30.40` beats `1.3.6.1.4.1.311.999` (10 segments > 8 segments) - -### Implementation Details - -The Azure Trusted Signing provider uses [`AzureTrustedSigningDidX509Generator`](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs), which: -- Extends the base `DidX509Generator` class and reuses its hash computation and formatting -- Calls the base class once to generate the properly formatted DID with subject policy -- Detects Microsoft EKUs (starting with `1.3.6.1.4.1.311`) in the leaf certificate -- Replaces the `::subject:...` portion with `::eku:{oid}` when Microsoft EKUs are present -- Returns the base implementation unchanged when no Microsoft EKUs are found -- Ensures base64url encoding (43 characters for SHA256) per RFC 4648 Section 5 -- Provides virtual methods for customization if needed - -### Example DID Formats - -**Without Microsoft EKUs (subject-based format):** -``` -did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:CN:MyCompany:O:Example -``` - -**With Microsoft EKU (Azure Trusted Signing specific format):** -``` -did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13 -``` - -Note the base64url-encoded hash is exactly 43 characters for SHA256 (not 64-character hex encoding). - -This enhanced format provides additional identity context for Azure Trusted Signing certificates, enabling better audit trails and certificate policy identification in SCITT-compliant systems. - -## Plugin Integration -For command-line usage, this library is integrated into CoseSignTool through the **CoseSignTool.AzureTrustedSigning.Plugin**. The plugin automatically handles: -- Azure authentication via `DefaultAzureCredential` -- Certificate retrieval and chain building -- Signing operations with cloud-based keys -- Automatic DID:X509:0 generation with EKU support for SCITT compliance - -See the [CoseSignTool documentation](https://github.com/microsoft/CoseSignTool) for command-line usage examples. - -## Example Usage -### Creating a Signing Key Provider -```csharp -using System; -using Azure.Developer.TrustedSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; - -public class Example -{ - public void CreateSigningKeyProvider() - { - // Note: In .NET 8.0, you must properly configure the AzSignContext - // with endpoint URL, account name, and certificate profile name - // This example shows the structure; actual initialization requires valid Azure credentials - - AzSignContext signContext = new AzSignContext(); - - // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); - - Console.WriteLine("Azure Trusted Signing key provider created successfully."); - } -} -``` -### Retrieving the Signing Certificate -```csharp -using System; -using Azure.Developer.TrustedSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; - -public class Example -{ - public void GetSigningCertificate() - { - // Initialize the Azure Trusted Signing context - AzSignContext signContext = new AzSignContext(); - - // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); - - // Retrieve the signing certificate - var signingCertificate = keyProvider.GetSigningCertificate(); - - Console.WriteLine($"Signing certificate retrieved: {signingCertificate.Subject}"); - } -} -``` -### Retrieving the Certificate Chain -```csharp -using System; -using System.Linq; -using Azure.Developer.TrustedSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; -using System.Security.Cryptography.X509Certificates; - -public class Example -{ - public void GetCertificateChain() - { - // Initialize the Azure Trusted Signing context - AzSignContext signContext = new AzSignContext(); - - // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); - - // Retrieve the certificate chain in leaf-first order - var certificateChain = keyProvider.GetCertificateChain(X509ChainSortOrder.LeafFirst).ToList(); - - Console.WriteLine("Certificate chain retrieved:"); - foreach (var cert in certificateChain) - { - Console.WriteLine($"- {cert.Subject}"); - } - } -} -``` -### Using RSA for Signing -```csharp -using System; -using Azure.Developer.TrustedSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; -using System.Security.Cryptography; - -public class Example -{ - public void UseRSAKey() - { - // Initialize the Azure Trusted Signing context - AzSignContext signContext = new AzSignContext(); - - // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); - - // Retrieve the RSA key - RSA rsaKey = keyProvider.ProvideRSAKey(); - - Console.WriteLine("RSA key retrieved successfully."); - } -} -``` -## Advanced: Custom DID Generation - -While the default EKU-based DID generation is suitable for most scenarios, you can customize the behavior by inheriting from `AzureTrustedSigningDidX509Generator`: - -```csharp -using CoseSign1.Certificates.AzureTrustedSigning; - -public class CustomAzureDidGenerator : AzureTrustedSigningDidX509Generator -{ - // Override to customize EKU extraction logic - protected override List ExtractEkus(X509Certificate2 certificate) - { - // Custom EKU extraction logic - return base.ExtractEkus(certificate); - } - - // Override to customize EKU selection algorithm - protected override string SelectDeepestGreatestEku(List ekus) - { - // Custom selection logic - return base.SelectDeepestGreatestEku(ekus); - } -} -``` - -## Limitations -- **ECDsa Not Supported**: The `ProvideECDsaKey` method is not supported for Azure Trusted Signing and will throw a `NotSupportedException`. -- **.NET 8.0 Only**: This library requires .NET 8.0 runtime and is not compatible with .NET Standard 2.0 or earlier .NET versions. -- **RSA Only**: Only RSA-based signing algorithms are supported by Azure Trusted Signing. -- **Cloud Connectivity Required**: Active internet connection to Azure Trusted Signing service is required for all signing operations. -- **DID Format**: The EKU-based DID format is specific to Azure Trusted Signing and may not be recognized by systems expecting standard subject-based DIDs. -## Conclusion -The [AzureTrustedSigningCoseSigningKeyProvider](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs) class provides a robust integration with the Azure Trusted Signing service, enabling seamless retrieval of signing certificates, certificate chains, and RSA keys. It is a powerful tool for creating and managing CoseSign1Message objects in environments that leverage Azure Trusted Signing. -
-
+# [CoseSign1.Certificates.AzureArtifactSigning](https://github.com/microsoft/CoseSignTool/tree/main/CoseSign1.Certificates.AzureArtifactSigning) + +**CoseSign1.Certificates.AzureArtifactSigning** is a .NET 8.0 library that provides an implementation of the `CertificateCoseSigningKeyProvider` class for Azure Trusted Signing. This library integrates with the Azure Trusted Signing service to provide signing certificates, certificate chains, and cryptographic keys. + +## Requirements +- **.NET 8.0 Runtime**: This library requires .NET 8.0 or later. +- **Azure Artifact Signing Account**: An active Azure Artifact Signing account with configured certificate profiles. +- **Azure Authentication**: Valid Azure credentials (Azure CLI, Managed Identity, environment variables, etc.). + +## Dependencies +**CoseSign1.Certificates.AzureArtifactSigning** has the following package dependencies: +* **CoseSign1.Certificates** - Base certificate provider abstractions +* **Azure.Developer.ArtifactSigning.CryptoProvider** - Azure Artifact Signing SDK for cryptographic operations +* **Azure.Identity** (transitive) - Azure authentication mechanisms +* **Azure.Core** (transitive) - Azure SDK core functionality + +## Creation +The following class is provided for creating a proper `CoseSign1Message` object signed by an Azure Artifact Signing certificate. + +### [AzureArtifactSigningCoseSigningKeyProvider](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs) +This class is a concrete implementation of `CertificateCoseSigningKeyProvider` that integrates with the Azure Artifact Signing service. It provides the following functionality: +* Retrieves the signing certificate from the Azure Artifact Signing service. +* Retrieves the certificate chain from the Azure Artifact Signing service. +* Provides an RSA key for signing or verification operations. + +#### Key Features: +- **GetSigningCertificate**: Retrieves the signing certificate from the Azure Artifact Signing service. +- **GetCertificateChain**: Retrieves the certificate chain in the desired sort order (root-first or leaf-first). +- **ProvideRSAKey**: Provides an RSA key for signing or verification operations. +- **ProvideECDsaKey**: Not supported for Azure Artifact Signing and will throw a `NotSupportedException`. +- **Issuer Property (SCITT)**: Overrides the base class to provide Azure-specific DID:X509:0 format with EKU support for non-standard certificates. + +#### Constructor: +- `AzureArtifactSigningCoseSigningKeyProvider(AzSignContext signContext)`: Initializes a new instance of the class using the provided `AzSignContext`. +#### Exceptions: +- `ArgumentNullException`: Thrown if the `signContext` parameter is null. +- `InvalidOperationException`: Thrown if the signing certificate or certificate chain is not available or is empty. +- `NotSupportedException`: Thrown when attempting to use ECDsa keys, as they are not supported. + +## SCITT Compliance and DID:X509:0 Support + +Azure Artifact Signing certificates automatically support **SCITT (Supply Chain Integrity, Transparency, and Trust)** compliance through an enhanced DID:X509:0 format that includes Extended Key Usage (EKU) information per the [DID:X509 specification](https://github.com/microsoft/did-x509/blob/main/specification.md#eku-policy). + +### DID:X509:0 Format with EKU + +Azure Artifact Signing certificates include **Microsoft-specific EKUs** (Enhanced Key Usages starting with `1.3.6.1.4.1.311`) that identify the certificate's intended purpose. When these Microsoft EKUs are present, the `Issuer` property automatically generates an EKU-based DID identifier: + +``` +did:x509:0:sha256:{base64url-hash}::eku:{oid} +``` + +Where: +- `{base64url-hash}` is the base64url-encoded root certificate fingerprint (43 characters for SHA256 per RFC 4648 Section 5) +- `{oid}` is the EKU OID in dotted decimal notation (e.g., `1.3.6.1.4.1.311.10.3.13`) +- The OID is NOT percent-encoded (just the raw OID string) + +#### Microsoft EKU Detection +The generator detects any EKU starting with the **Microsoft reserved prefix `1.3.6.1.4.1.311`**. When Microsoft EKUs are present, an EKU-based DID is generated. When no Microsoft EKUs are found, the generator falls back to the standard subject-based DID format. + +#### Deepest Greatest EKU Selection +When multiple Microsoft EKUs are present in an Azure Trusted Signing certificate, the "deepest greatest" EKU is selected using: +1. **Depth Priority**: EKU with the most OID segments (e.g., `1.3.6.1.4.1.311.20.30` has 9 segments) +2. **Last Segment Tiebreaker**: If multiple EKUs have the same depth, the one with the largest last segment value is selected + +**Examples:** +- `1.3.6.1.4.1.311.20.99` beats `1.3.6.1.4.1.311.20.50` (same depth, 99 > 50) +- `1.3.6.1.4.1.311.20.30.40` beats `1.3.6.1.4.1.311.999` (10 segments > 8 segments) + +### Implementation Details + +The Azure Trusted Signing provider uses [`AzureTrustedSigningDidX509Generator`](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs), which: +- Extends the base `DidX509Generator` class and reuses its hash computation and formatting +- Calls the base class once to generate the properly formatted DID with subject policy +- Detects Microsoft EKUs (starting with `1.3.6.1.4.1.311`) in the leaf certificate +- Replaces the `::subject:...` portion with `::eku:{oid}` when Microsoft EKUs are present +- Returns the base implementation unchanged when no Microsoft EKUs are found +- Ensures base64url encoding (43 characters for SHA256) per RFC 4648 Section 5 +- Provides virtual methods for customization if needed + +### Example DID Formats + +**Without Microsoft EKUs (subject-based format):** +``` +did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:CN:MyCompany:O:Example +``` + +**With Microsoft EKU (Azure Trusted Signing specific format):** +``` +did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13 +``` + +Note the base64url-encoded hash is exactly 43 characters for SHA256 (not 64-character hex encoding). + +This enhanced format provides additional identity context for Azure Trusted Signing certificates, enabling better audit trails and certificate policy identification in SCITT-compliant systems. + +## Plugin Integration +For command-line usage, this library is integrated into CoseSignTool through the **CoseSignTool.AzureTrustedSigning.Plugin**. The plugin automatically handles: +- Azure authentication via `DefaultAzureCredential` +- Certificate retrieval and chain building +- Signing operations with cloud-based keys +- Automatic DID:X509:0 generation with EKU support for SCITT compliance + +See the [CoseSignTool documentation](https://github.com/microsoft/CoseSignTool) for command-line usage examples. + +## Example Usage +### Creating a Signing Key Provider +```csharp +using System; +using Azure.Developer.ArtifactSigning.CryptoProvider; +using CoseSign1.Certificates.AzureTrustedSigning; + +public class Example +{ + public void CreateSigningKeyProvider() + { + // Note: In .NET 8.0, you must properly configure the AzSignContext + // with endpoint URL, account name, and certificate profile name + // This example shows the structure; actual initialization requires valid Azure credentials + + AzSignContext signContext = new AzSignContext(); + + // Create the signing key provider + AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + + Console.WriteLine("Azure Trusted Signing key provider created successfully."); + } +} +``` +### Retrieving the Signing Certificate +```csharp +using System; +using Azure.Developer.ArtifactSigning.CryptoProvider; +using CoseSign1.Certificates.AzureTrustedSigning; + +public class Example +{ + public void GetSigningCertificate() + { + // Initialize the Azure Trusted Signing context + AzSignContext signContext = new AzSignContext(); + + // Create the signing key provider + AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + + // Retrieve the signing certificate + var signingCertificate = keyProvider.GetSigningCertificate(); + + Console.WriteLine($"Signing certificate retrieved: {signingCertificate.Subject}"); + } +} +``` +### Retrieving the Certificate Chain +```csharp +using System; +using System.Linq; +using Azure.Developer.ArtifactSigning.CryptoProvider; +using CoseSign1.Certificates.AzureTrustedSigning; +using System.Security.Cryptography.X509Certificates; + +public class Example +{ + public void GetCertificateChain() + { + // Initialize the Azure Trusted Signing context + AzSignContext signContext = new AzSignContext(); + + // Create the signing key provider + AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + + // Retrieve the certificate chain in leaf-first order + var certificateChain = keyProvider.GetCertificateChain(X509ChainSortOrder.LeafFirst).ToList(); + + Console.WriteLine("Certificate chain retrieved:"); + foreach (var cert in certificateChain) + { + Console.WriteLine($"- {cert.Subject}"); + } + } +} +``` +### Using RSA for Signing +```csharp +using System; +using Azure.Developer.TrustedSigning.CryptoProvider; +using CoseSign1.Certificates.AzureTrustedSigning; +using System.Security.Cryptography; + +public class Example +{ + public void UseRSAKey() + { + // Initialize the Azure Trusted Signing context + AzSignContext signContext = new AzSignContext(); + + // Create the signing key provider + AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + + // Retrieve the RSA key + RSA rsaKey = keyProvider.ProvideRSAKey(); + + Console.WriteLine("RSA key retrieved successfully."); + } +} +``` +## Advanced: Custom DID Generation + +While the default EKU-based DID generation is suitable for most scenarios, you can customize the behavior by inheriting from `AzureTrustedSigningDidX509Generator`: + +```csharp +using CoseSign1.Certificates.AzureTrustedSigning; + +public class CustomAzureDidGenerator : AzureTrustedSigningDidX509Generator +{ + // Override to customize EKU extraction logic + protected override List ExtractEkus(X509Certificate2 certificate) + { + // Custom EKU extraction logic + return base.ExtractEkus(certificate); + } + + // Override to customize EKU selection algorithm + protected override string SelectDeepestGreatestEku(List ekus) + { + // Custom selection logic + return base.SelectDeepestGreatestEku(ekus); + } +} +``` + +## Limitations +- **ECDsa Not Supported**: The `ProvideECDsaKey` method is not supported for Azure Trusted Signing and will throw a `NotSupportedException`. +- **.NET 8.0 Only**: This library requires .NET 8.0 runtime and is not compatible with .NET Standard 2.0 or earlier .NET versions. +- **RSA Only**: Only RSA-based signing algorithms are supported by Azure Trusted Signing. +- **Cloud Connectivity Required**: Active internet connection to Azure Trusted Signing service is required for all signing operations. +- **DID Format**: The EKU-based DID format is specific to Azure Trusted Signing and may not be recognized by systems expecting standard subject-based DIDs. +## Conclusion +The [AzureTrustedSigningCoseSigningKeyProvider](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs) class provides a robust integration with the Azure Trusted Signing service, enabling seamless retrieval of signing certificates, certificate chains, and RSA keys. It is a powerful tool for creating and managing CoseSign1Message objects in environments that leverage Azure Trusted Signing. +
+
For more information, refer to the [CoseSign1.Certificates](https://github.com/microsoft/CoseSignTool/blob/main/docs/CoseSign1.Certificates.md). \ No newline at end of file diff --git a/docs/Plugins.md b/docs/Plugins.md index 7d6e3a3c..a5e3f3b5 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -1311,7 +1311,7 @@ CoseSignTool.AzureTrustedSigning.Plugin/ └── [Dependencies copied to plugins/CoseSignTool.AzureTrustedSigning.Plugin/] ├── Azure.CodeSigning.dll ├── Azure.Core.dll - ├── Azure.Developer.TrustedSigning.CryptoProvider.dll + ├── Azure.Developer.ArtifactSigning.CryptoProvider.dll └── ... ``` From d61491cc4d2fef7f633f2ec95bdd3521392afb77 Mon Sep 17 00:00:00 2001 From: Jaxel Rojas Lopez Date: Tue, 10 Mar 2026 23:22:39 -0400 Subject: [PATCH 2/4] docs: update docs to replace ats with aas --- ...Sign1.Certificates.AzureArtifactSigning.md | 55 +++++---- docs/CoseSign1.Certificates.md | 4 +- docs/Plugins.md | 105 +++++++++--------- docs/SCITTCompliance.md | 8 +- 4 files changed, 85 insertions(+), 87 deletions(-) diff --git a/docs/CoseSign1.Certificates.AzureArtifactSigning.md b/docs/CoseSign1.Certificates.AzureArtifactSigning.md index 743d0138..3f59507b 100644 --- a/docs/CoseSign1.Certificates.AzureArtifactSigning.md +++ b/docs/CoseSign1.Certificates.AzureArtifactSigning.md @@ -1,6 +1,6 @@ # [CoseSign1.Certificates.AzureArtifactSigning](https://github.com/microsoft/CoseSignTool/tree/main/CoseSign1.Certificates.AzureArtifactSigning) -**CoseSign1.Certificates.AzureArtifactSigning** is a .NET 8.0 library that provides an implementation of the `CertificateCoseSigningKeyProvider` class for Azure Trusted Signing. This library integrates with the Azure Trusted Signing service to provide signing certificates, certificate chains, and cryptographic keys. +**CoseSign1.Certificates.AzureArtifactSigning** is a .NET 8.0 library that provides an implementation of the `CertificateCoseSigningKeyProvider` class for Azure Artifact Signing. This library integrates with the Azure Artifact Signing service to provide signing certificates, certificate chains, and cryptographic keys. ## Requirements - **.NET 8.0 Runtime**: This library requires .NET 8.0 or later. @@ -58,7 +58,7 @@ Where: The generator detects any EKU starting with the **Microsoft reserved prefix `1.3.6.1.4.1.311`**. When Microsoft EKUs are present, an EKU-based DID is generated. When no Microsoft EKUs are found, the generator falls back to the standard subject-based DID format. #### Deepest Greatest EKU Selection -When multiple Microsoft EKUs are present in an Azure Trusted Signing certificate, the "deepest greatest" EKU is selected using: +When multiple Microsoft EKUs are present in an Azure Artifact Signing certificate, the "deepest greatest" EKU is selected using: 1. **Depth Priority**: EKU with the most OID segments (e.g., `1.3.6.1.4.1.311.20.30` has 9 segments) 2. **Last Segment Tiebreaker**: If multiple EKUs have the same depth, the one with the largest last segment value is selected @@ -68,7 +68,7 @@ When multiple Microsoft EKUs are present in an Azure Trusted Signing certificate ### Implementation Details -The Azure Trusted Signing provider uses [`AzureTrustedSigningDidX509Generator`](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs), which: +The Azure Artifact Signing provider uses [`AzureArtifactSigningDidX509Generator`](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningDidX509Generator.cs), which: - Extends the base `DidX509Generator` class and reuses its hash computation and formatting - Calls the base class once to generate the properly formatted DID with subject policy - Detects Microsoft EKUs (starting with `1.3.6.1.4.1.311`) in the leaf certificate @@ -84,17 +84,17 @@ The Azure Trusted Signing provider uses [`AzureTrustedSigningDidX509Generator`]( did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:CN:MyCompany:O:Example ``` -**With Microsoft EKU (Azure Trusted Signing specific format):** +**With Microsoft EKU (Azure Artifact Signing specific format):** ``` did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13 ``` Note the base64url-encoded hash is exactly 43 characters for SHA256 (not 64-character hex encoding). -This enhanced format provides additional identity context for Azure Trusted Signing certificates, enabling better audit trails and certificate policy identification in SCITT-compliant systems. +This enhanced format provides additional identity context for Azure Artifact Signing certificates, enabling better audit trails and certificate policy identification in SCITT-compliant systems. ## Plugin Integration -For command-line usage, this library is integrated into CoseSignTool through the **CoseSignTool.AzureTrustedSigning.Plugin**. The plugin automatically handles: +For command-line usage, this library is integrated into CoseSignTool through the **CoseSignTool.AzureArtifactSigning.Plugin**. The plugin automatically handles: - Azure authentication via `DefaultAzureCredential` - Certificate retrieval and chain building - Signing operations with cloud-based keys @@ -107,7 +107,7 @@ See the [CoseSignTool documentation](https://github.com/microsoft/CoseSignTool) ```csharp using System; using Azure.Developer.ArtifactSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; +using CoseSign1.Certificates.AzureArtifactSigning; public class Example { @@ -120,9 +120,9 @@ public class Example AzSignContext signContext = new AzSignContext(); // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + AzureArtifactSigningCoseSigningKeyProvider keyProvider = new AzureArtifactSigningCoseSigningKeyProvider(signContext); - Console.WriteLine("Azure Trusted Signing key provider created successfully."); + Console.WriteLine("Azure Artifact Signing key provider created successfully."); } } ``` @@ -130,18 +130,17 @@ public class Example ```csharp using System; using Azure.Developer.ArtifactSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; +using CoseSign1.Certificates.AzureArtifactSigning; public class Example { public void GetSigningCertificate() { - // Initialize the Azure Trusted Signing context + // Initialize the Azure Artifact Signing context AzSignContext signContext = new AzSignContext(); // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); - + AzureArtifactSigningCoseSigningKeyProvider keyProvider = new AzureArtifactSigningCoseSigningKeyProvider(signContext); // Retrieve the signing certificate var signingCertificate = keyProvider.GetSigningCertificate(); @@ -154,18 +153,18 @@ public class Example using System; using System.Linq; using Azure.Developer.ArtifactSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; +using CoseSign1.Certificates.AzureArtifactSigning; using System.Security.Cryptography.X509Certificates; public class Example { public void GetCertificateChain() { - // Initialize the Azure Trusted Signing context + // Initialize the Azure Artifact Signing context AzSignContext signContext = new AzSignContext(); // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + AzureArtifactSigningCoseSigningKeyProvider keyProvider = new AzureArtifactSigningCoseSigningKeyProvider(signContext); // Retrieve the certificate chain in leaf-first order var certificateChain = keyProvider.GetCertificateChain(X509ChainSortOrder.LeafFirst).ToList(); @@ -181,19 +180,19 @@ public class Example ### Using RSA for Signing ```csharp using System; -using Azure.Developer.TrustedSigning.CryptoProvider; -using CoseSign1.Certificates.AzureTrustedSigning; +using Azure.Developer.ArtifactSigning.CryptoProvider; +using CoseSign1.Certificates.AzureArtifactSigning; using System.Security.Cryptography; public class Example { public void UseRSAKey() { - // Initialize the Azure Trusted Signing context + // Initialize the Azure Artifact Signing context AzSignContext signContext = new AzSignContext(); // Create the signing key provider - AzureTrustedSigningCoseSigningKeyProvider keyProvider = new AzureTrustedSigningCoseSigningKeyProvider(signContext); + AzureArtifactSigningCoseSigningKeyProvider keyProvider = new AzureArtifactSigningCoseSigningKeyProvider(signContext); // Retrieve the RSA key RSA rsaKey = keyProvider.ProvideRSAKey(); @@ -204,12 +203,12 @@ public class Example ``` ## Advanced: Custom DID Generation -While the default EKU-based DID generation is suitable for most scenarios, you can customize the behavior by inheriting from `AzureTrustedSigningDidX509Generator`: +While the default EKU-based DID generation is suitable for most scenarios, you can customize the behavior by inheriting from `AzureArtifactSigningDidX509Generator`: ```csharp -using CoseSign1.Certificates.AzureTrustedSigning; +using CoseSign1.Certificates.AzureArtifactSigning; -public class CustomAzureDidGenerator : AzureTrustedSigningDidX509Generator +public class CustomAzureDidGenerator : AzureArtifactSigningDidX509Generator { // Override to customize EKU extraction logic protected override List ExtractEkus(X509Certificate2 certificate) @@ -228,13 +227,13 @@ public class CustomAzureDidGenerator : AzureTrustedSigningDidX509Generator ``` ## Limitations -- **ECDsa Not Supported**: The `ProvideECDsaKey` method is not supported for Azure Trusted Signing and will throw a `NotSupportedException`. +- **ECDsa Not Supported**: The `ProvideECDsaKey` method is not supported for Azure Artifact Signing and will throw a `NotSupportedException`. - **.NET 8.0 Only**: This library requires .NET 8.0 runtime and is not compatible with .NET Standard 2.0 or earlier .NET versions. -- **RSA Only**: Only RSA-based signing algorithms are supported by Azure Trusted Signing. -- **Cloud Connectivity Required**: Active internet connection to Azure Trusted Signing service is required for all signing operations. -- **DID Format**: The EKU-based DID format is specific to Azure Trusted Signing and may not be recognized by systems expecting standard subject-based DIDs. +- **RSA Only**: Only RSA-based signing algorithms are supported by Azure Artifact Signing. +- **Cloud Connectivity Required**: Active internet connection to Azure Artifact Signing service is required for all signing operations. +- **DID Format**: The EKU-based DID format is specific to Azure Artifact Signing and may not be recognized by systems expecting standard subject-based DIDs. ## Conclusion -The [AzureTrustedSigningCoseSigningKeyProvider](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningCoseSigningKeyProvider.cs) class provides a robust integration with the Azure Trusted Signing service, enabling seamless retrieval of signing certificates, certificate chains, and RSA keys. It is a powerful tool for creating and managing CoseSign1Message objects in environments that leverage Azure Trusted Signing. +The [AzureArtifactSigningCoseSigningKeyProvider](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs) class provides a robust integration with the Azure Artifact Signing service, enabling seamless retrieval of signing certificates, certificate chains, and RSA keys. It is a powerful tool for creating and managing CoseSign1Message objects in environments that leverage Azure Artifact Signing.

For more information, refer to the [CoseSign1.Certificates](https://github.com/microsoft/CoseSignTool/blob/main/docs/CoseSign1.Certificates.md). \ No newline at end of file diff --git a/docs/CoseSign1.Certificates.md b/docs/CoseSign1.Certificates.md index 77b3bae8..299a3e69 100644 --- a/docs/CoseSign1.Certificates.md +++ b/docs/CoseSign1.Certificates.md @@ -151,7 +151,7 @@ Utility class for generating **DID:x509 identifiers** from X.509 certificates fo - **Subject Policy Format**: Uses key:value pairs separated by colons - **Proper Percent-Encoding**: Only ALPHA, DIGIT, '-', '.', '_' allowed unencoded (tilde NOT allowed) - **Multiple Hash Algorithms**: Supports SHA-256, SHA-384, and SHA-512 -- **Extensible**: Can be inherited to implement custom DID generation behaviors (e.g., Azure Trusted Signing's EKU-based format) +- **Extensible**: Can be inherited to implement custom DID generation behaviors (e.g., Azure Artifact Signing's EKU-based format) #### DID:x509 Format: ``` @@ -169,7 +169,7 @@ Example: did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:O:GitHub:CN:User ``` -> **Note**: Azure Trusted Signing uses an enhanced format that includes EKU information for non-standard certificates. See [AzureTrustedSigningDidX509Generator](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureTrustedSigning/AzureTrustedSigningDidX509Generator.cs) and [CoseSign1.Certificates.AzureTrustedSigning.md](CoseSign1.Certificates.AzureTrustedSigning.md#scitt-compliance-and-didx5090-support) for details. +> **Note**: Azure Artifact Signing uses an enhanced format that includes EKU information for non-standard certificates. See [AzureArtifactSigningDidX509Generator](https://github.com/microsoft/CoseSignTool/blob/main/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningDidX509Generator.cs) and [CoseSign1.Certificates.AzureArtifactSigning.md](CoseSign1.Certificates.AzureArtifactSigning.md#scitt-compliance-and-didx5090-support) for details. #### Usage: diff --git a/docs/Plugins.md b/docs/Plugins.md index a5e3f3b5..9e0dbae1 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -58,7 +58,7 @@ The interface for certificate provider plugins that extend signing capabilities: ```csharp public interface ICertificateProviderPlugin { - string ProviderName { get; } // Unique provider identifier (e.g., "azure-trusted-signing") + string ProviderName { get; } // Unique provider identifier (e.g., "azure-artifact-signing") string Description { get; } // Provider description for help output IDictionary GetProviderOptions(); // Provider-specific command-line options bool CanCreateProvider(IConfiguration configuration); // Check if required parameters are present @@ -68,7 +68,7 @@ public interface ICertificateProviderPlugin **Key Concepts:** - **Provider Name**: Lowercase, hyphenated identifier used with `--cert-provider` parameter -- **Provider Options**: Custom command-line parameters specific to the provider (e.g., `--ats-endpoint`) +- **Provider Options**: Custom command-line parameters specific to the provider (e.g., `--aas-endpoint`) - **Signing Key Provider**: Returns an `ICoseSigningKeyProvider` for certificate-based signing operations - **Configuration-Based**: Uses `IConfiguration` to access command-line parameters and settings @@ -77,10 +77,10 @@ Certificate provider plugins integrate seamlessly with the built-in `sign` and p ```bash # Using with built-in sign command -CoseSignTool sign --payload file.txt --cert-provider azure-trusted-signing --ats-endpoint https://... --ats-account-name myaccount +CoseSignTool sign --payload file.txt --cert-provider azure-artifact-signing --aas-endpoint https://... --aas-account-name myaccount # Using with indirect-sign plugin command -CoseSignTool indirect-sign --payload file.txt --signature file.cose --cert-provider azure-trusted-signing --ats-endpoint https://... +CoseSignTool indirect-sign --payload file.txt --signature file.cose --cert-provider azure-artifact-signing --aas-endpoint https://... ``` #### IPluginCommand @@ -386,7 +386,7 @@ Certificate provider plugins extend the signing capabilities of CoseSignTool by Certificate provider plugins are ideal for: - **Cloud HSM Integration**: Azure Key Vault, AWS KMS, Google Cloud KMS -- **Remote Signing Services**: Azure Trusted Signing, DigiCert ONE, GlobalSign DSS +- **Remote Signing Services**: Azure Artifact Signing, DigiCert ONE, GlobalSign DSS - **Hardware Security Modules**: Thales Luna, Utimaco, nCipher - **Smart Cards and Tokens**: YubiKey, TPM, PIV cards - **Custom Key Management**: Proprietary key storage and signing infrastructure @@ -890,18 +890,18 @@ public class YourCertProviderPluginTests } ``` -### Example: Azure Trusted Signing Certificate Provider +### Example: Azure Artifact Signing Certificate Provider For a complete, production-ready example of a certificate provider plugin, see: -- **Source Code**: `CoseSignTool.AzureTrustedSigning.Plugin/` +- **Source Code**: `CoseSignTool.AzureArtifactSigning.Plugin/` - **Documentation**: [CertificateProviders.md](CertificateProviders.md) -- **Usage Guide**: [CoseSign1.Certificates.AzureTrustedSigning.md](CoseSign1.Certificates.AzureTrustedSigning.md) +- **Usage Guide**: [CoseSign1.Certificates.AzureArtifactSigning.md](CoseSign1.Certificates.AzureArtifactSigning.md) -The Azure Trusted Signing plugin demonstrates: +The Azure Artifact Signing plugin demonstrates: - Integration with Azure cloud-based signing service - DefaultAzureCredential authentication - Comprehensive error handling -- Provider-specific parameters (`--ats-endpoint`, `--ats-account-name`, `--ats-cert-profile-name`) +- Provider-specific parameters (`--aas-endpoint`, `--aas-account-name`, `--aas-cert-profile-name`) - Full test coverage ## Plugin Security Model @@ -1166,19 +1166,19 @@ CoseSignTool your_register --help CoseSignTool --help # Output: # Certificate Providers: -# azure-trusted-signing Azure Trusted Signing cloud-based certificate provider +# azure-artifact-signing Azure Artifact Signing cloud-based certificate provider # Sign command help shows certificate providers CoseSignTool sign --help # Output includes: # Certificate Providers: # The following certificate provider plugins are available for signing: -# azure-trusted-signing Azure Trusted Signing cloud-based certificate provider -# Usage: CoseSignTool sign --cert-provider azure-trusted-signing [options] +# azure-artifact-signing Azure Artifact Signing cloud-based certificate provider +# Usage: CoseSignTool sign --cert-provider azure-artifact-signing [options] # Options: -# --ats-endpoint -# --ats-account-name -# --ats-cert-profile-name +# --aas-endpoint +# --aas-account-name +# --aas-cert-profile-name # Indirect-sign command help also shows certificate providers CoseSignTool indirect-sign --help @@ -1277,38 +1277,37 @@ CoseSignTool mst_register --endpoint https://your-mst-instance.azure.com --paylo CoseSignTool mst_register --endpoint https://your-cts-instance.azure.com --payload file.txt --signature file.cose ``` -### Certificate Provider Plugin: Azure Trusted Signing +### Certificate Provider Plugin: Azure Artifact Signing -The CoseSignTool includes a production-ready certificate provider plugin for Azure Trusted Signing, demonstrating best practices for cloud-based signing services. - -> **📖 Complete Documentation**: For comprehensive Azure Trusted Signing documentation, including setup, authentication, and advanced scenarios, see [CertificateProviders.md](CertificateProviders.md) and [CoseSign1.Certificates.AzureTrustedSigning.md](CoseSign1.Certificates.AzureTrustedSigning.md). +The CoseSignTool includes a production-ready certificate provider plugin for Azure Artifact Signing, demonstrating best practices for cloud-based signing services. +> **📖 Complete Documentation**: For comprehensive Azure Artifact Signing documentation, including setup, authentication, and advanced scenarios, see [CertificateProviders.md](CertificateProviders.md) and [CoseSign1.Certificates.AzureArtifactSigning.md](CoseSign1.Certificates.AzureArtifactSigning.md). #### Quick Start -The Azure Trusted Signing plugin integrates with the `sign` and `indirect-sign` commands: +The Azure Artifact Signing plugin integrates with the `sign` and `indirect-sign` commands: ```bash -# Sign with Azure Trusted Signing (using DefaultAzureCredential) +# Sign with Azure Artifact Signing (using DefaultAzureCredential) CoseSignTool sign --payload myfile.txt \ - --cert-provider azure-trusted-signing \ - --ats-endpoint https://myaccount.codesigning.azure.net \ - --ats-account-name myaccount \ - --ats-cert-profile-name myprofile + --cert-provider azure-artifact-signing \ + --aas-endpoint https://myaccount.codesigning.azure.net \ + --aas-account-name myaccount \ + --aas-cert-profile-name myprofile -# Indirect sign with Azure Trusted Signing +# Indirect sign with Azure Artifact Signing CoseSignTool indirect-sign --payload myfile.txt --signature myfile.cose \ - --cert-provider azure-trusted-signing \ - --ats-endpoint https://myaccount.codesigning.azure.net \ - --ats-account-name myaccount \ - --ats-cert-profile-name myprofile + --cert-provider azure-artifact-signing \ + --aas-endpoint https://myaccount.codesigning.azure.net \ + --aas-account-name myaccount \ + --aas-cert-profile-name myprofile ``` #### Plugin Structure ``` -CoseSignTool.AzureTrustedSigning.Plugin/ -├── AzureTrustedSigningPlugin.cs # Main plugin implementation (ICertificateProviderPlugin) -├── CoseSignTool.AzureTrustedSigning.Plugin.csproj # Project with dependency isolation -└── [Dependencies copied to plugins/CoseSignTool.AzureTrustedSigning.Plugin/] +CoseSignTool.AzureArtifactSigning.Plugin/ +├── AzureArtifactSigningPlugin.cs # Main plugin implementation (ICertificateProviderPlugin) +├── CoseSignTool.AzureArtifactSigning.Plugin.csproj # Project with dependency isolation +└── [Dependencies copied to plugins/CoseSignTool.AzureArtifactSigning.Plugin/] ├── Azure.CodeSigning.dll ├── Azure.Core.dll ├── Azure.Developer.ArtifactSigning.CryptoProvider.dll @@ -1320,13 +1319,13 @@ CoseSignTool.AzureTrustedSigning.Plugin/ 1. **Dependency Isolation**: All Azure dependencies packaged in plugin subdirectory 2. **Secure Authentication**: DefaultAzureCredential for passwordless auth 3. **Provider-Specific Options**: Custom parameters (`--ats-*`) with prefix to avoid conflicts -4. **Integration with CoseSign1.Certificates**: Uses `AzureTrustedSigningCoseSigningKeyProvider` +4. **Integration with CoseSign1.Certificates**: Uses `AzureArtifactSigningCoseSigningKeyProvider` 5. **Comprehensive Testing**: Full unit test coverage demonstrating plugin testing patterns #### Authentication Flow ```csharp -// Azure Trusted Signing uses DefaultAzureCredential: +// Azure Artifact Signing uses DefaultAzureCredential: // 1. Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET) // 2. Managed Identity (when running in Azure) // 3. Visual Studio / VS Code credentials @@ -1335,10 +1334,10 @@ CoseSignTool.AzureTrustedSigning.Plugin/ // Never requires credentials on command line CoseSignTool sign --payload file.txt \ - --cert-provider azure-trusted-signing \ - --ats-endpoint https://myaccount.codesigning.azure.net \ - --ats-account-name myaccount \ - --ats-cert-profile-name myprofile + --cert-provider azure-artifact-signing \ + --aas-endpoint https://myaccount.codesigning.azure.net \ + --aas-account-name myaccount \ + --aas-cert-profile-name myprofile ``` #### Usage in CI/CD @@ -1349,29 +1348,29 @@ CoseSignTool sign --payload file.txt \ with: creds: ${{ secrets.AZURE_CREDENTIALS }} -- name: Sign with Azure Trusted Signing +- name: Sign with Azure Artifact Signing run: | CoseSignTool sign --payload artifact.bin \ - --cert-provider azure-trusted-signing \ - --ats-endpoint ${{ secrets.ATS_ENDPOINT }} \ - --ats-account-name ${{ secrets.ATS_ACCOUNT }} \ - --ats-cert-profile-name ${{ secrets.ATS_PROFILE }} + --cert-provider azure-artifact-signing \ + --aas-endpoint ${{ secrets.AAS_ENDPOINT }} \ + --aas-account-name ${{ secrets.AAS_ACCOUNT }} \ + --aas-cert-profile-name ${{ secrets.AAS_PROFILE }} ``` **Azure DevOps:** ```yaml - task: AzureCLI@2 - displayName: 'Sign with Azure Trusted Signing' + displayName: 'Sign with Azure Artifact Signing' inputs: azureSubscription: 'MyServiceConnection' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | CoseSignTool sign --payload artifact.bin \ - --cert-provider azure-trusted-signing \ - --ats-endpoint $(ATS_ENDPOINT) \ - --ats-account-name $(ATS_ACCOUNT) \ - --ats-cert-profile-name $(ATS_PROFILE) + --cert-provider azure-artifact-signing \ + --aas-endpoint $(AAS_ENDPOINT) \ + --aas-account-name $(AAS_ACCOUNT) \ + --aas-cert-profile-name $(AAS_PROFILE) ``` ## Best Practices @@ -1388,8 +1387,8 @@ CoseSignTool sign --payload file.txt \ ### Certificate Provider Plugin Development 1. **Secure Credentials**: Use `DefaultAzureCredential`, environment variables, or OS credential stores - never command-line parameters -2. **Provider Naming**: Use lowercase with hyphens (e.g., `azure-trusted-signing`, not `AzureTrustedSigning`) -3. **Option Prefixing**: Prefix provider-specific options to avoid conflicts (e.g., `--ats-endpoint`, not `--endpoint`) +2. **Provider Naming**: Use lowercase with hyphens (e.g., `azure-artifact-signing`, not `AzureArtifactSigning`) +3. **Option Prefixing**: Prefix provider-specific options to avoid conflicts (e.g., `--aas-endpoint`, not `--endpoint`) 4. **Validation**: Implement `CanCreateProvider` to quickly validate required parameters 5. **Remote Signing**: Implement custom `AsymmetricAlgorithm` subclass for HSM/remote signing 6. **Certificate Chains**: Include intermediate certificates via `AdditionalCertificates` property diff --git a/docs/SCITTCompliance.md b/docs/SCITTCompliance.md index 6b6a0c3a..edcfcc54 100644 --- a/docs/SCITTCompliance.md +++ b/docs/SCITTCompliance.md @@ -77,15 +77,15 @@ var customProvider = new CustomCertificateProvider(cert); var headerExtender = customProvider.CreateHeaderExtenderWithCWTClaims(); ``` -### Azure Trusted Signing: Enhanced DID:X509:0 Format +### Azure Artifact Signing: Enhanced DID:X509:0 Format -**Azure Trusted Signing** certificates include Microsoft-specific Enhanced Key Usage (EKU) extensions that identify certificate purposes. When Microsoft EKUs are detected, a specialized EKU-based DID format is generated per the [DID:X509 EKU Policy specification](https://github.com/microsoft/did-x509/blob/main/specification.md#eku-policy): +**Azure Artifact Signing** certificates include Microsoft-specific Enhanced Key Usage (EKU) extensions that identify certificate purposes. When Microsoft EKUs are detected, a specialized EKU-based DID format is generated per the [DID:X509 EKU Policy specification](https://github.com/microsoft/did-x509/blob/main/specification.md#eku-policy): ``` did:x509:0:sha256:{base64url-hash}::eku:{oid} ``` -This Azure Trusted Signing specific format: +This Azure Artifact Signing specific format: - **Uses base64url encoding**: Certificate hash is base64url-encoded (43 characters for SHA256, not 64-character hex) - **Detects Microsoft EKUs**: Checks for any EKU starting with `1.3.6.1.4.1.311` - **Generates EKU-based DID**: When Microsoft EKUs are present, includes the deepest greatest EKU in the DID @@ -100,7 +100,7 @@ This Azure Trusted Signing specific format: did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13 ``` -For more details, see [CoseSign1.Certificates.AzureTrustedSigning.md](CoseSign1.Certificates.AzureTrustedSigning.md#scitt-compliance-and-didx5090-support). +For more details, see [CoseSign1.Certificates.AzureArtifactSigning.md](CoseSign1.Certificates.AzureArtifactSigning.md#scitt-compliance-and-didx5090-support). ## Using SCITT Compliance in CoseSignTool From fdb18a5fc6259e7b1dc94a5399148491984b1ea2 Mon Sep 17 00:00:00 2001 From: Jaxel Rojas Lopez Date: Thu, 12 Mar 2026 16:49:12 -0400 Subject: [PATCH 3/4] tests: update mock to use interface instead of concrete class --- ...ifactSigningCoseSigningKeyProviderTests.cs | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs index 79ce2476..ef749535 100644 --- a/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs +++ b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs @@ -11,6 +11,7 @@ namespace CoseSign1.Certificates.AzureArtifactSigning.Tests; 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; @@ -45,7 +46,8 @@ public void Constructor_ThrowsArgumentNullException_WhenSignContextIsNull() public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateChainIsUnavailable() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); + mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns((IReadOnlyList?)null); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -63,7 +65,7 @@ public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateC public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateChainIsEmpty() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(new List()); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -85,7 +87,7 @@ public void GetCertificateChain_ThrowsInvalidOperationException_WhenCertificateC public void GetCertificateChain_ReturnsChainInCorrectOrder(X509ChainSortOrder sortOrder, bool reverseOrder) { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); List mockChain = CreateMockCertificateChain(); mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(mockChain); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -106,7 +108,7 @@ public void GetCertificateChain_ReturnsChainInCorrectOrder(X509ChainSortOrder so public void GetSigningCertificate_ThrowsInvalidOperationException_WhenSigningCertificateIsUnavailable() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); mockSignContext.Setup(context => context.GetSigningCertificate(It.IsAny())).Returns((X509Certificate2?)null); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -124,7 +126,7 @@ public void GetSigningCertificate_ThrowsInvalidOperationException_WhenSigningCer public void GetSigningCertificate_ReturnsSigningCertificate() { // Arrange - Mock mockSignContext = new Mock(); + 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); @@ -144,7 +146,7 @@ public void GetSigningCertificate_ReturnsSigningCertificate() public void ProvideECDsaKey_ThrowsNotSupportedException() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); // Act & Assert @@ -161,7 +163,7 @@ public void ProvideECDsaKey_ThrowsNotSupportedException() public void ProvideRSAKey_ReturnsRSAInstance() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); // Act @@ -180,7 +182,7 @@ public void ProvideRSAKey_ReturnsRSAInstance() public void GetCertificateChain_CachesChainOnFirstCall() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); List mockChain = CreateMockCertificateChain(); mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(mockChain); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -202,7 +204,7 @@ public void GetCertificateChain_CachesChainOnFirstCall() public void GetSigningCertificate_CachesCertificateOnFirstCall() { // Arrange - Mock mockSignContext = new Mock(); + 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); @@ -224,7 +226,7 @@ public void GetSigningCertificate_CachesCertificateOnFirstCall() public void ProvideRSAKey_CachesRSAInstanceOnFirstCall() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); // Act @@ -245,7 +247,7 @@ public void ProvideRSAKey_CachesRSAInstanceOnFirstCall() public void ProvideRSAKey_ReturnsSameCachedInstance_RegardlessOfPublicKeyParameter() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); // Act @@ -266,7 +268,7 @@ public void ProvideRSAKey_ReturnsSameCachedInstance_RegardlessOfPublicKeyParamet public void GetCertificateChain_IsThreadSafe_UnderConcurrentAccess() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); List mockChain = CreateMockCertificateChain(); int callCount = 0; mockSignContext.Setup(context => context.GetCertChain(It.IsAny())) @@ -305,7 +307,7 @@ public void GetCertificateChain_IsThreadSafe_UnderConcurrentAccess() public void GetCertificateChain_HandlesRootCertificateCorrectly() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); // Create a certificate (CreateCertificate creates self-signed certs) X509Certificate2 rootCert = TestCertificateUtils.CreateCertificate(nameof(GetCertificateChain_HandlesRootCertificateCorrectly)); List chain = new List { rootCert }; @@ -335,7 +337,7 @@ public void GetCertificateChain_HandlesRootCertificateCorrectly() public void ProvideECDsaKey_ThrowsNotSupportedException_WithPublicKeyParameterTrue() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); // Act & Assert @@ -351,7 +353,7 @@ public void ProvideECDsaKey_ThrowsNotSupportedException_WithPublicKeyParameterTr public void ProvideRSAKey_WorksWithPublicKeyParameterTrue() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); // Act @@ -379,7 +381,7 @@ private static List CreateMockCertificateChain([CallerMemberNa public void Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); // Create a certificate chain with non-standard EKU X509Certificate2 leafCert = TestCertificateUtils.CreateCertificate(nameof(Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat)); @@ -407,7 +409,7 @@ public void Issuer_WithNonStandardEku_ReturnsDidX509WithEkuFormat() public void Issuer_WhenCertificateChainUnavailable_ReturnsNull() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns((IReadOnlyList?)null); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -425,7 +427,7 @@ public void Issuer_WhenCertificateChainUnavailable_ReturnsNull() public void Issuer_WhenCertificateChainEmpty_ReturnsNull() { // Arrange - Mock mockSignContext = new Mock(); + Mock mockSignContext = new Mock(); mockSignContext.Setup(context => context.GetCertChain(It.IsAny())).Returns(new List()); AzureArtifactSigningCoseSigningKeyProvider provider = new AzureArtifactSigningCoseSigningKeyProvider(mockSignContext.Object); @@ -443,7 +445,7 @@ public void Issuer_WhenCertificateChainEmpty_ReturnsNull() public void Issuer_UsesAzureArtifactSigningDidGenerator() { // Arrange - Mock mockSignContext = new Mock(); + 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); @@ -464,7 +466,7 @@ public void Issuer_UsesAzureArtifactSigningDidGenerator() public void Issuer_OnException_FallsBackToBaseImplementation() { // Arrange - Mock mockSignContext = new Mock(); + 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")); From f7c0b5acc50c027f52ebf78f147a29fd4815bfd1 Mon Sep 17 00:00:00 2001 From: Jaxel Rojas Lopez Date: Sun, 15 Mar 2026 20:24:00 -0400 Subject: [PATCH 4/4] fix: merge conflicts --- .github/workflows/dotnet.yml | 50 +++--- ...ifactSigningCoseSigningKeyProviderTests.cs | 2 +- ...reArtifactSigningCoseSigningKeyProvider.cs | 9 +- ...ignTool.AzureArtifactSigning.Plugin.csproj | 10 +- CoseSignTool/SignCommand.cs | 152 +++++++++--------- Directory.Packages.props | 2 +- README.md | 12 +- docs/CertificateProviders.md | 100 ++++++------ docs/CoseSignTool.md | 46 +++--- 9 files changed, 192 insertions(+), 191 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3b7b9bb0..b7d42333 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 }} @@ -95,7 +95,7 @@ jobs: - name: Checkout code if: ${{ github.event_name == 'pull_request' }} uses: actions/checkout@v2 - + # Sync the changelog version. - name: Fetch and checkout if: ${{ github.event_name == 'pull_request' }} @@ -311,33 +311,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)" @@ -348,21 +348,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 { @@ -372,13 +372,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." @@ -407,11 +407,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" @@ -424,16 +424,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 \ @@ -441,7 +441,7 @@ jobs: --property:VersionNgt=$VERSION_WITHOUT_V \ --output published/packages \ --verbosity minimal - + if [ $? -eq 0 ]; then echo "✅ Successfully created package for $project_name" else @@ -451,7 +451,7 @@ jobs: echo "⚠️ Project file not found: $project" fi done - + # List created packages echo "" echo "📋 Created NuGet packages:" @@ -462,7 +462,7 @@ jobs: else echo "❌ No packages directory found" fi - + echo "🎯 NuGet package creation completed." shell: bash diff --git a/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs index ef749535..6e19b284 100644 --- a/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs +++ b/CoseSign1.Certificates.AzureArtifactSigning.Tests/AzureArtifactSigningCoseSigningKeyProviderTests.cs @@ -35,7 +35,7 @@ public void Constructor_ThrowsArgumentNullException_WhenSignContextIsNull() // Act & Assert Assert.That( () => new AzureArtifactSigningCoseSigningKeyProvider(null), - Throws.TypeOf().With.Property("ParamName").EqualTo("signContext")); + Throws.TypeOf().With.Property("ParamName").EqualTo("ISignContext")); } /// diff --git a/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs b/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs index 77cab78f..c93ac290 100644 --- a/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs +++ b/CoseSign1.Certificates.AzureArtifactSigning/AzureArtifactSigningCoseSigningKeyProvider.cs @@ -8,6 +8,7 @@ namespace CoseSign1.Certificates.AzureArtifactSigning; using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography; using Azure.Developer.ArtifactSigning.CryptoProvider; +using Azure.Developer.ArtifactSigning.CryptoProvider.Interfaces; using CoseSign1.Certificates.Local; using System.Linq; @@ -18,17 +19,17 @@ namespace CoseSign1.Certificates.AzureArtifactSigning; /// public class AzureArtifactSigningCoseSigningKeyProvider : CertificateCoseSigningKeyProvider { - private readonly AzSignContext SignContext; + private readonly ISignContext SignContext; private static readonly AzureArtifactSigningDidX509Generator AzureDidGenerator = new(); /// /// Initializes a new instance of the class. /// - /// The used to interact with Azure Artifact Signing. + /// The used to interact with Azure Artifact Signing. /// Thrown if is null. - public AzureArtifactSigningCoseSigningKeyProvider(AzSignContext signContext) + public AzureArtifactSigningCoseSigningKeyProvider(ISignContext signContext) { - SignContext = signContext ?? throw new ArgumentNullException(nameof(signContext)); + SignContext = signContext ?? throw new ArgumentNullException(nameof(ISignContext)); } /// diff --git a/CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj b/CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj index fe0a46fe..5687529a 100644 --- a/CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj +++ b/CoseSignTool.AzureArtifactSigning.Plugin/CoseSignTool.AzureArtifactSigning.Plugin.csproj @@ -12,16 +12,16 @@ latest true true - CoseSignTool.AzureTrustedSigning.Plugin + CoseSignTool.AzureArtifactSigning.Plugin true True ..\StrongNameKeys\35MSSharedLib1024.snk - + true true true - + false @@ -32,7 +32,7 @@ none compile; build; native; contentfiles; analyzers - + true none compile; build; native; contentfiles; analyzers @@ -40,7 +40,7 @@ - + true diff --git a/CoseSignTool/SignCommand.cs b/CoseSignTool/SignCommand.cs index b0bf59df..00b985b1 100644 --- a/CoseSignTool/SignCommand.cs +++ b/CoseSignTool/SignCommand.cs @@ -332,17 +332,17 @@ public override ExitCode Run() // Create a cancellation token with timeout (default 30 seconds from MaxWaitTime) using CancellationTokenSource timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(MaxWaitTime)); - + // Generate the COSE signature asynchronously with cancellation support. ReadOnlyMemory signedBytes = CoseHandler.SignAsync( - payloadStream, - signingKeyProvider, - EmbedPayload, - SignatureFile, - ContentType ?? CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, + payloadStream, + signingKeyProvider, + EmbedPayload, + SignatureFile, + ContentType ?? CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE, headerExtender, timeoutCts.Token).ConfigureAwait(false).GetAwaiter().GetResult(); - + // Write the signature to stream or file. if (PipeOutput) { @@ -354,7 +354,7 @@ public override ExitCode Run() File.WriteAllBytes(SignatureFile!.FullName, signedBytes.ToArray()); } - return ExitCode.Success; + return ExitCode.Success; } catch (ArgumentException ex) { @@ -384,13 +384,13 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p PfxCertificate = GetOptionString(provider, nameof(PfxCertificate)); PemCertificate = GetOptionString(provider, nameof(PemCertificate)); PemKey = GetOptionString(provider, nameof(PemKey)); - + // Password handling: direct from command line (for PFX backward compat) // or via environment variable / interactive prompt (preferred for PEM) Password = GetOptionString(provider, nameof(Password)); PasswordEnvVar = GetOptionString(provider, nameof(PasswordEnvVar)); PasswordPrompt = GetOptionBool(provider, nameof(PasswordPrompt)); - + ContentType = GetOptionString(provider, nameof(ContentType), CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE); StoreName = GetOptionString(provider, nameof(StoreName), DefaultStoreName); string? sl = GetOptionString(provider, nameof(StoreLocation), DefaultStoreLocation); @@ -408,7 +408,7 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p // IntUnProtectedHeaders GetOptionHeadersFromCommandLine(provider, "IntUnProtectedHeaders", false, HeaderValueConverter, IntHeaders); } - + if (StringHeaders == null) { StringHeaders = new(); @@ -424,7 +424,7 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p CwtIssuer = GetOptionString(provider, nameof(CwtIssuer)); CwtSubject = GetOptionString(provider, nameof(CwtSubject)); CwtAudience = GetOptionString(provider, nameof(CwtAudience)); - + // Custom CWT claims (can be specified multiple times) if (provider.TryGet(nameof(CwtClaims), out string? cwtClaimsValue) && !string.IsNullOrWhiteSpace(cwtClaimsValue)) { @@ -437,7 +437,7 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p index++; } } - + // Enable SCITT compliance by default bool scittComplianceSet = provider.TryGet(nameof(EnableScittCompliance), out string? scittValue); EnableScittCompliance = !scittComplianceSet || (bool.TryParse(scittValue, out bool scittResult) && scittResult); @@ -462,29 +462,29 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p Console.Error.WriteLine("Error: --pw/--Password is only supported for PFX files. For encrypted PEM private keys, use --pwenv/--PasswordEnvVar or --pwprompt/--PasswordPrompt."); return null; } - + // For PEM files, check environment variable string envVarName = PasswordEnvVar ?? DefaultPasswordEnvVar; - + // Try to get password from environment variable string? envPassword = Environment.GetEnvironmentVariable(envVarName); if (!string.IsNullOrEmpty(envPassword)) { return envPassword; } - + // If PasswordEnvVar was explicitly set but the variable is empty/missing, that's an error if (PasswordEnvVar != null) { Console.Error.WriteLine($"Warning: Environment variable '{PasswordEnvVar}' is not set or empty."); } - + // If interactive prompt is requested, prompt for password if (PasswordPrompt) { return PromptForPassword(); } - + // No password provided return null; } @@ -503,12 +503,12 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p } Console.Error.Write("Enter password: "); - + StringBuilder password = new StringBuilder(); while (true) { ConsoleKeyInfo key = Console.ReadKey(intercept: true); - + if (key.Key == ConsoleKey.Enter) { Console.Error.WriteLine(); @@ -528,7 +528,7 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p Console.Error.Write("*"); } } - + return password.Length > 0 ? password.ToString() : null; } @@ -706,7 +706,7 @@ private static TypeV HeaderValueConverter(string[]? labelValue = null) { X509Certificate2 cert; List? additionalRoots = null; - + if (PemCertificate is not null) { // Load from PEM files (common on Linux/Unix systems) @@ -716,24 +716,24 @@ private static TypeV HeaderValueConverter(string[]? labelValue = null) { // Load the PFX certificate. This will throw a CryptographicException if the password is wrong or missing. ThrowIfMissing(PfxCertificate, "Could not find the certificate file"); - + // Load the PFX as a certificate store to extract all certificates X509Certificate2Collection pfxCertificates = []; pfxCertificates.Import(PfxCertificate, Password, X509KeyStorageFlags.Exportable); // Build the certificate chain in leaf-first order List chainedCertificates = BuildCertificateChain(pfxCertificates, Thumbprint); - + if (chainedCertificates.Count == 0) { - throw new CoseSign1CertificateException(string.IsNullOrEmpty(Thumbprint) + throw new CoseSign1CertificateException(string.IsNullOrEmpty(Thumbprint) ? "No valid certificate chain found in PFX file" : $"No certificate with private key and thumbprint '{Thumbprint}' found in PFX file"); } - + // The first certificate in the chain is the signing certificate (leaf) cert = chainedCertificates[0]; - + // The remaining certificates are the chain (intermediate and root) additionalRoots = chainedCertificates.Skip(1).ToList(); } @@ -757,13 +757,13 @@ private static TypeV HeaderValueConverter(string[]? labelValue = null) private (X509Certificate2 certificate, List? additionalRoots) LoadCertFromPem() { ThrowIfMissing(PemCertificate!, "Could not find the PEM certificate file"); - + // Read the PEM certificate file string certPem = File.ReadAllText(PemCertificate!); - + // Parse all certificates from the PEM file (may contain a chain) List certificates = ParsePemCertificates(certPem); - + if (certificates.Count == 0) { throw new CryptographicException($"No valid certificates found in PEM file: {PemCertificate}"); @@ -771,7 +771,7 @@ private static TypeV HeaderValueConverter(string[]? labelValue = null) // The first certificate is typically the leaf/signing certificate X509Certificate2 leafCert = certificates[0]; - + // If a separate key file is provided, load and combine with the certificate if (PemKey is not null) { @@ -784,17 +784,17 @@ private static TypeV HeaderValueConverter(string[]? labelValue = null) // Try to find the private key in the same PEM file as the certificate leafCert = LoadCertificateWithPrivateKey(leafCert, certPem); } - + if (!leafCert.HasPrivateKey) { throw new CryptographicException( "The certificate does not have a private key. " + "Specify the private key file using --key or include it in the PEM certificate file."); } - + // Additional certificates in the PEM file are treated as the certificate chain - List? additionalRoots = certificates.Count > 1 - ? certificates.Skip(1).ToList() + List? additionalRoots = certificates.Count > 1 + ? certificates.Skip(1).ToList() : null; return (leafCert, additionalRoots); @@ -808,11 +808,11 @@ private static TypeV HeaderValueConverter(string[]? labelValue = null) private static List ParsePemCertificates(string pem) { List certificates = []; - + // Match all certificate blocks in the PEM const string certHeader = "-----BEGIN CERTIFICATE-----"; const string certFooter = "-----END CERTIFICATE-----"; - + int startIndex = 0; while ((startIndex = pem.IndexOf(certHeader, startIndex, StringComparison.Ordinal)) >= 0) { @@ -821,10 +821,10 @@ private static List ParsePemCertificates(string pem) { break; } - + endIndex += certFooter.Length; string certBlock = pem.Substring(startIndex, endIndex - startIndex); - + try { X509Certificate2 cert = X509Certificate2.CreateFromPem(certBlock); @@ -834,10 +834,10 @@ private static List ParsePemCertificates(string pem) { // Skip invalid certificate blocks } - + startIndex = endIndex; } - + return certificates; } @@ -976,13 +976,13 @@ private ICoseSigningKeyProvider LoadSigningKeyProviderFromPlugin() // Create and return the provider // Note: We could pass a logger here if we had one available ICoseSigningKeyProvider provider = plugin.CreateProvider(configuration, logger: null); - + // If the provider is a certificate-based provider, set the EnableScittCompliance flag if (provider is CoseSign1.Certificates.CertificateCoseSigningKeyProvider certProvider) { certProvider.EnableScittCompliance = EnableScittCompliance; } - + return provider; } @@ -1010,7 +1010,7 @@ private static List BuildCertificateChain(X509Certificate2Coll { List certList = certificates.Cast().ToList(); List result = new List(); - + // If a specific thumbprint is provided, start with that certificate X509Certificate2? leafCert = null; if (!string.IsNullOrEmpty(targetThumbprint)) @@ -1035,7 +1035,7 @@ private static List BuildCertificateChain(X509Certificate2Coll .Where(c => !IsIssuerOfAnyCertificate(c, certList)) .FirstOrDefault(); } - + if (leafCert == null) { return result; // No suitable leaf certificate found @@ -1044,19 +1044,19 @@ private static List BuildCertificateChain(X509Certificate2Coll // Build the chain starting from the leaf X509Certificate2? current = leafCert; HashSet usedCerts = new HashSet(); - + while (current != null && !usedCerts.Contains(current.Thumbprint)) { result.Add(current); usedCerts.Add(current.Thumbprint); - + // Find the issuer of the current certificate current = FindIssuer(current, certList.Where(c => !usedCerts.Contains(c.Thumbprint))); } - + return result; } - + /// /// Checks if a certificate is the issuer of any certificate in the collection. /// @@ -1067,7 +1067,7 @@ private static bool IsIssuerOfAnyCertificate(X509Certificate2 potentialIssuer, L { return certificates.Any(cert => cert != potentialIssuer && IsIssuer(potentialIssuer, cert)); } - + /// /// Finds the issuer certificate for a given certificate. /// @@ -1078,7 +1078,7 @@ private static bool IsIssuerOfAnyCertificate(X509Certificate2 potentialIssuer, L { return candidates.FirstOrDefault(issuer => IsIssuer(issuer, certificate)); } - + /// /// Checks if one certificate is the issuer of another certificate by verifying the cryptographic signature. /// @@ -1092,13 +1092,13 @@ private static bool IsIssuer(X509Certificate2 issuer, X509Certificate2 subject) { return false; } - + // Handle self-signed certificates if (issuer.Equals(subject)) { return issuer.SubjectName.Name.Equals(issuer.IssuerName.Name, StringComparison.OrdinalIgnoreCase); } - + // Verify the cryptographic signature try { @@ -1107,7 +1107,7 @@ private static bool IsIssuer(X509Certificate2 issuer, X509Certificate2 subject) chain.ChainPolicy.ExtraStore.Add(issuer); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - + // Build the chain and check if the issuer is in the chain if (chain.Build(subject)) { @@ -1120,7 +1120,7 @@ private static bool IsIssuer(X509Certificate2 issuer, X509Certificate2 subject) } } } - + // Fallback: manual signature verification for edge cases return VerifySignatureManually(issuer, subject); } @@ -1137,7 +1137,7 @@ private static bool IsIssuer(X509Certificate2 issuer, X509Certificate2 subject) } } } - + /// /// Manually verifies if an issuer certificate signed a subject certificate. /// @@ -1169,14 +1169,14 @@ private static bool VerifySignatureManually(X509Certificate2 issuer, X509Certifi { return false; } - + // Parse the certificate to extract TBS and signature (byte[] tbsData, byte[] signatureData) = ExtractTbsAndSignature(rawData); if (tbsData == null || signatureData == null) { return false; } - + // Verify the signature based on the algorithm return signatureAlgorithm.Value switch { @@ -1192,7 +1192,7 @@ private static bool VerifySignatureManually(X509Certificate2 issuer, X509Certifi return false; } } - + /// /// Extracts the To-Be-Signed (TBS) data and signature from a certificate's raw data. /// @@ -1209,13 +1209,13 @@ private static (byte[]? tbsData, byte[]? signatureData) ExtractTbsAndSignature(b // Read the TBS certificate (first element) by marking position and reading System.Formats.Asn1.AsnReader tbsReader = certSequence.ReadSequence(); byte[] tbsData = tbsReader.ReadEncodedValue().ToArray(); - + // Skip the signature algorithm identifier (second element) certSequence.ReadSequence(); // Read the signature value (third element) byte[] signatureData = certSequence.ReadBitString(out _); - + return (tbsData, signatureData); } catch @@ -1223,7 +1223,7 @@ private static (byte[]? tbsData, byte[]? signatureData) ExtractTbsAndSignature(b return (null, null); } } - + /// /// Verifies an RSA-SHA256 signature. /// @@ -1236,7 +1236,7 @@ private static bool VerifyRsaSha256Signature(PublicKey publicKey, byte[] tbsData { return false; } - + return rsa.VerifyData(tbsData, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } catch @@ -1244,7 +1244,7 @@ private static bool VerifyRsaSha256Signature(PublicKey publicKey, byte[] tbsData return false; } } - + /// /// Verifies an RSA-SHA1 signature. /// @@ -1257,7 +1257,7 @@ private static bool VerifyRsaSha1Signature(PublicKey publicKey, byte[] tbsData, { return false; } - + return rsa.VerifyData(tbsData, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); // CodeQL [SM02196] This is to support certificates or other singers which use SHA1 with RSA for validation only. } catch @@ -1265,7 +1265,7 @@ private static bool VerifyRsaSha1Signature(PublicKey publicKey, byte[] tbsData, return false; } } - + /// /// Verifies an ECDSA-SHA256 signature. /// @@ -1278,7 +1278,7 @@ private static bool VerifyEcdsaSha256Signature(PublicKey publicKey, byte[] tbsDa { return false; } - + return ecdsa.VerifyData(tbsData, signature, HashAlgorithmName.SHA256); } catch @@ -1286,7 +1286,7 @@ private static bool VerifyEcdsaSha256Signature(PublicKey publicKey, byte[] tbsDa return false; } } - + /// /// Verifies an ECDSA-SHA1 signature. /// @@ -1299,7 +1299,7 @@ private static bool VerifyEcdsaSha1Signature(PublicKey publicKey, byte[] tbsData { return false; } - + return ecdsa.VerifyData(tbsData, signature, HashAlgorithmName.SHA1); // CodeQL [SM02196] This is to support certificates or other singers which use SHA1 with ECDSA for validation only. } catch @@ -1329,7 +1329,7 @@ Default value is [payload file].cose. A signing certificate from one of the following sources: - --CertProvider, -cp: Use a certificate provider plugin such as Azure Trusted Signing or custom HSM providers. + --CertProvider, -cp: Use a certificate provider plugin such as Azure Artifact Signing or custom HSM providers. See Certificate Providers section below for available providers. --OR-- @@ -1363,7 +1363,7 @@ Default value is 'CurrentUser'. For PEM files with encrypted private keys, use secure password options instead: - --PasswordEnvVar, --pwenv: The name of an environment variable containing the password. + --PasswordEnvVar, --pwenv: The name of an environment variable containing the password. If not specified, defaults to checking COSESIGNTOOL_PASSWORD environment variable. Example: --pwenv MY_CERT_PASSWORD @@ -1398,7 +1398,7 @@ Options to enable SCITT (Supply Chain Integrity, Transparency, and Trust) compli --CwtClaims, --cwt: Optional. Custom CWT claims as label:value pairs. Can be specified multiple times. Labels can be integers (e.g., ""100:custom-value"") or RFC 8392 claim names (iss, sub, aud, exp, nbf, iat, cti). Timestamp claims (exp, nbf, iat) accept date/time strings (e.g., ""2024-12-31T23:59:59Z"") or Unix timestamps. - Examples: + Examples: --cwt ""cti:abc123"" --cwt ""100:custom-value"" --cwt ""exp:2024-12-31T23:59:59Z"" --cwt ""iss:custom-issuer"" --cwt ""sub:custom-subject"" --cwt ""nbf:1735689600"" @@ -1416,7 +1416,7 @@ Both the label and value are strings. --StringProtectedHeaders, -sph: A collection of name-value pairs with a string label and value. Sample input: --sph message-type=cose,customer-name=contoso - + --IntUnProtectedHeaders, -iuh: A collection of name-value pairs with a string label and an int32 value. Sample input: --iuh created-at=12345678,customer-count=10 @@ -1450,12 +1450,12 @@ private static string GetCertificateProviderUsageString() sb.AppendLine("======================"); sb.AppendLine(); sb.AppendLine("Available certificate provider plugins:"); - + foreach (var kvp in CoseSignTool.CertificateProviderManager.Providers) { sb.AppendLine($" {kvp.Key,-30} {kvp.Value.Description}"); } - + sb.AppendLine(); sb.AppendLine("To use a certificate provider, specify the --cert-provider option:"); sb.AppendLine(" --cert-provider "); @@ -1464,7 +1464,7 @@ private static string GetCertificateProviderUsageString() sb.AppendLine("For detailed information about a specific provider, use:"); sb.AppendLine(" CoseSignTool help "); sb.AppendLine(); - + return sb.ToString(); } } diff --git a/Directory.Packages.props b/Directory.Packages.props index b5fa83a6..92b92210 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + diff --git a/README.md b/README.md index 512f943c..d6e75f9b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ CoseSignTool and CoseHandler support three commands/methods: Additionally, CoseSignTool supports: - **Plugin System**: Extend the tool with custom commands and third-party integrations (Microsoft's Signing Transparency, etc.) - **Certificate Provider Plugins**: Use cloud-based signing services, HSMs, or custom certificate sources - - Built-in support for **Azure Trusted Signing** (Microsoft's managed signing service) + - Built-in support for **Azure Artifact Signing** (Microsoft's managed signing service) - Extensible architecture for custom certificate providers - See [CertificateProviders.md](./docs/CertificateProviders.md) for details - **SCITT Compliance**: Automatic CWT (CBOR Web Token) Claims with DID:x509 identifiers for supply chain transparency @@ -61,12 +61,12 @@ CoseSignTool sign --payload payload.txt --pfx mycert.pfx --SignatureFile signatu CoseSignTool sign --payload payload.txt --pfx mycert.pfx --SignatureFile signature.cose \ --scitt false -# Using Azure Trusted Signing (cloud-based signing) +# Using Azure Artifact Signing (cloud-based signing) CoseSignTool sign --payload payload.txt --SignatureFile signature.cose \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile + --cp azure-artifact-signing \ + --aas-endpoint https://contoso.codesigning.azure.net \ + --aas-account-name ContosoAccount \ + --aas-cert-profile-name ContosoProfile ``` For complete documentation, see [SCITTCompliance.md](./docs/SCITTCompliance.md) and [CertificateProviders.md](./docs/CertificateProviders.md) diff --git a/docs/CertificateProviders.md b/docs/CertificateProviders.md index 3721a8e8..63c5a42d 100644 --- a/docs/CertificateProviders.md +++ b/docs/CertificateProviders.md @@ -6,7 +6,7 @@ CoseSignTool supports an extensible **Certificate Provider Plugin Architecture** - [Overview](#overview) - [Built-in Providers](#built-in-providers) - [Using Certificate Providers](#using-certificate-providers) -- [Azure Trusted Signing](#azure-trusted-signing) +- [Azure Artifact Signing](#azure-artifact-signing) - [Prerequisites](#prerequisites) - [Authentication](#authentication) - [Usage Examples](#usage-examples) @@ -36,13 +36,13 @@ When no `--cp` is specified, CoseSignTool uses local certificate loading: - **PFX files**: Load certificates with private keys from `.pfx` files - **Certificate stores**: Access certificates from Windows/macOS/Linux certificate stores -### Azure Trusted Signing +### Azure Artifact Signing 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 Trusted Signing](#azure-trusted-signing) section for details. +See [Azure Artifact Signing](#azure-artifact-signing) section for details. ## Using Certificate Providers @@ -57,33 +57,33 @@ CoseSignTool sign --help # Shows all available certificate providers and their parameters ``` -### Example with Azure Trusted Signing +### Example with Azure Artifact Signing ```bash CoseSignTool sign \ --p payload.txt \ --sf signature.cose \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile + --cp azure-artifact-signing \ + --aas-endpoint https://contoso.codesigning.azure.net \ + --aas-account-name ContosoAccount \ + --aas-cert-profile-name ContosoProfile ``` -## Azure Trusted Signing +## Azure Artifact Signing -Azure Trusted Signing is Microsoft's cloud-based code signing service that provides secure, compliant signing without managing certificates locally. +Azure Artifact Signing is Microsoft's cloud-based code signing service that provides secure, compliant signing without managing certificates locally. ### Prerequisites 1. **Azure Subscription**: Active Azure subscription with billing enabled -2. **Azure Trusted Signing Account**: Created in Azure Portal +2. **Azure Artifact Signing Account**: Created in Azure Portal 3. **Certificate Profile**: Configured with appropriate certificate type 4. **Permissions**: Your Azure identity must have: - `Code Signing Certificate Profile Signer` role on the certificate profile - - Access to the Azure Trusted Signing account + - Access to the Azure Artifact Signing account ### Authentication -Azure Trusted Signing uses **Azure DefaultAzureCredential** for authentication, which automatically tries authentication methods in this order: +Azure Artifact Signing uses **Azure DefaultAzureCredential** for authentication, which automatically tries authentication methods in this order: 1. **Environment Variables** (recommended for CI/CD) ```bash @@ -122,10 +122,10 @@ az login CoseSignTool sign \ --p document.pdf \ --sf document.pdf.cose \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile + --cp azure-artifact-signing \ + --aas-endpoint https://contoso.codesigning.azure.net \ + --aas-account-name ContosoAccount \ + --aas-cert-profile-name ContosoProfile ``` #### CI/CD Pipeline (GitHub Actions) @@ -150,10 +150,10 @@ jobs: CoseSignTool sign \ --p release-artifact.bin \ --sf release-artifact.bin.cose \ - --cp azure-trusted-signing \ - --ats-endpoint ${{ secrets.ATS_ENDPOINT }} \ - --ats-account-name ${{ secrets.ATS_ACCOUNT_NAME }} \ - --ats-cert-profile-name ${{ secrets.ATS_CERT_PROFILE_NAME }} + --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 }} ``` #### Azure DevOps Pipeline @@ -176,10 +176,10 @@ steps: CoseSignTool sign \ --p $(Build.ArtifactStagingDirectory)/artifact.bin \ --sf $(Build.ArtifactStagingDirectory)/artifact.bin.cose \ - --cp azure-trusted-signing \ - --ats-endpoint $(ATS_ENDPOINT) \ - --ats-account-name $(ATS_ACCOUNT_NAME) \ - --ats-cert-profile-name $(ATS_CERT_PROFILE_NAME) + --cp azure-artifact-signing \ + --aas-endpoint $(AAS_ENDPOINT) \ + --aas-account-name $(AAS_ACCOUNT_NAME) \ + --aas-cert-profile-name $(AAS_CERT_PROFILE_NAME) ``` #### Embedded Signature with SCITT Claims @@ -188,10 +188,10 @@ CoseSignTool sign \ --p payload.txt \ --sf payload.cose \ --ep \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile \ + --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" @@ -199,10 +199,10 @@ CoseSignTool sign \ #### Batch Signing with Environment Variables ```bash -# Set Azure Trusted Signing configuration -export ATS_ENDPOINT="https://contoso.codesigning.azure.net" -export ATS_ACCOUNT_NAME="ContosoAccount" -export ATS_CERT_PROFILE_NAME="ContosoProfile" +# 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" @@ -214,22 +214,22 @@ for file in *.bin; do CoseSignTool sign \ --p "$file" \ --sf "${file}.cose" \ - --cp azure-trusted-signing \ - --ats-endpoint "$ATS_ENDPOINT" \ - --ats-account-name "$ATS_ACCOUNT_NAME" \ - --ats-cert-profile-name "$ATS_CERT_PROFILE_NAME" + --cp azure-artifact-signing \ + --aas-endpoint "$AAS_ENDPOINT" \ + --aas-account-name "$AAS_ACCOUNT_NAME" \ + --aas-cert-profile-name "$AAS_CERT_PROFILE_NAME" done ``` -### Azure Trusted Signing Parameters +### Azure Artifact Signing Parameters | Parameter | Alias | Required | Description | |-----------|-------|----------|-------------| -| `--ats-endpoint` | | Yes | Azure Trusted Signing endpoint URL (e.g., `https://contoso.codesigning.azure.net`) | -| `--ats-account-name` | | Yes | Azure Trusted Signing account name | -| `--ats-cert-profile-name` | | Yes | Certificate profile name within the account | +| `--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 | -### Troubleshooting Azure Trusted Signing +### Troubleshooting Azure Artifact Signing #### Authentication Failures ``` @@ -265,15 +265,15 @@ az role assignment list \ #### Invalid Parameters ``` -Error: Certificate provider 'azure-trusted-signing' cannot create a provider with the given configuration. +Error: Certificate provider 'azure-artifact-signing' cannot create a provider with the given configuration. ``` **Solution**: Verify all required parameters are provided ```bash CoseSignTool sign \ - --cp azure-trusted-signing \ - --ats-endpoint "https://your-endpoint.codesigning.azure.net" \ - --ats-account-name "YourAccount" \ - --ats-cert-profile-name "YourProfile" \ + --cp azure-artifact-signing \ + --aas-endpoint "https://your-endpoint.codesigning.azure.net" \ + --aas-account-name "YourAccount" \ + --aas-cert-profile-name "YourProfile" \ --p test.txt ``` @@ -287,14 +287,14 @@ All certificate provider plugins must implement `ICertificateProviderPlugin`: public interface ICertificateProviderPlugin { /// - /// Gets the unique name of this certificate provider (e.g., "azure-trusted-signing"). + /// Gets the unique name of this certificate provider (e.g., "azure-artifact-signing"). /// Used with the --cp command line parameter. /// string ProviderName { get; } /// /// Gets the available command-line options for this provider. - /// Keys are option names (e.g., "--ats-endpoint"), values are descriptions. + /// Keys are option names (e.g., "--aas-endpoint"), values are descriptions. /// IReadOnlyDictionary GetProviderOptions(); @@ -691,5 +691,5 @@ logger?.LogInformation($"Certificate thumbprint: {cert.Thumbprint}"); - [CoseSignTool.md](./CoseSignTool.md) - Main CoseSignTool documentation - [Plugins.md](./Plugins.md) - General plugin development guide - [PluginNamingConventions.md](./PluginNamingConventions.md) - Plugin naming requirements -- [CoseSign1.Certificates.AzureTrustedSigning.md](./CoseSign1.Certificates.AzureTrustedSigning.md) - Azure Trusted Signing API documentation +- [CoseSign1.Certificates.AzureArtifactSigning.md](./CoseSign1.Certificates.AzureArtifactSigning.md) - Azure Artifact Signing API documentation - [SCITTCompliance.md](./SCITTCompliance.md) - SCITT compliance features diff --git a/docs/CoseSignTool.md b/docs/CoseSignTool.md index 457a35b1..dcd1ad96 100644 --- a/docs/CoseSignTool.md +++ b/docs/CoseSignTool.md @@ -44,7 +44,7 @@ The **Sign** command signs a file or stream. You will need to specify: * The payload content to sign. This may be a file specified with the **--PayloadFile** or **--p** option or you can pipe it in on the Standard Input channel when you call CoseSignTool. Piping in the content is generally considered more secure and performant option but large streams of > 2gb in length are not yet supported. * A signing key provider. You have four options: - 1. **Certificate Provider Plugin** (recommended for cloud/HSM signing): Use the **--CertProvider** or **--cp** option to specify a certificate provider plugin (e.g., `azure-trusted-signing`). See [Certificate Providers](#certificate-providers) section below. + 1. **Certificate Provider Plugin** (recommended for cloud/HSM signing): Use the **--CertProvider** or **--cp** option to specify a certificate provider plugin (e.g., `azure-artifact-signing`). See [Certificate Providers](#certificate-providers) section below. 2. **Local PFX Certificate** (common on Windows): Use the **--PfxCertificate** or **--pfx** option to point to a .pfx certificate file and **--Password** or **--pw** to provide the password if the file is password-protected. The certificate must include a private key. * **PFX Certificate Chain Handling**: When using a PFX file that contains multiple certificates (such as a complete certificate chain), CoseSignTool will automatically use all certificates in the PFX for proper chain building. If you specify a **--Thumbprint** or **--th** along with the PFX file, CoseSignTool will use the certificate matching that thumbprint for signing and treat the remaining certificates as additional roots for chain validation. If no thumbprint is specified, the first certificate with a private key will be used for signing. 3. **PEM Certificate Files** (common on Linux/Unix): Use the **--PemCertificate** or **--pem** option to point to a PEM-encoded certificate file. If the private key is in a separate file, use **--PemKey** or **--key** to specify the key file. For encrypted private keys, use **--PasswordEnvVar** / **--pwenv** to specify an environment variable containing the password, or **--PasswordPrompt** / **--pwprompt** to enter interactively. @@ -275,17 +275,17 @@ CoseSignTool supports an extensible **Certificate Provider Plugin Architecture** ### Available Certificate Providers -#### Azure Trusted Signing +#### Azure Artifact Signing Microsoft's cloud-based signing service providing managed certificates, FIPS 140-2 Level 3 HSM-backed signing, and seamless Azure integration. **Parameters:** -* **--CertProvider**, **--cp** - Set to `azure-trusted-signing` to use Azure Trusted Signing -* **--ats-endpoint** - Azure Trusted Signing endpoint URL (e.g., `https://contoso.codesigning.azure.net`) -* **--ats-account-name** - Azure Trusted Signing account name -* **--ats-cert-profile-name** - Certificate profile name within the account +* **--CertProvider**, **--cp** - Set to `azure-artifact-signing` to use Azure Artifact Signing +* **--aas-endpoint** - Azure Artifact Signing endpoint URL (e.g., `https://contoso.codesigning.azure.net`) +* **--aas-account-name** - Azure Artifact Signing account name +* **--aas-cert-profile-name** - Certificate profile name within the account **Authentication:** -Azure Trusted Signing uses Azure DefaultAzureCredential, which automatically tries: +Azure Artifact Signing uses Azure DefaultAzureCredential, which automatically tries: 1. Environment variables (`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`) 2. Managed Identity (for Azure VMs/containers) 3. Azure CLI (`az login`) @@ -298,10 +298,10 @@ Azure Trusted Signing uses Azure DefaultAzureCredential, which automatically tri # Basic usage with Azure CLI authentication az login CoseSignTool sign --p payload.txt --sf signature.cose \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile + --cp azure-artifact-signing \ + --aas-endpoint https://contoso.codesigning.azure.net \ + --aas-account-name ContosoAccount \ + --aas-cert-profile-name ContosoProfile ``` ```bash @@ -311,19 +311,19 @@ export AZURE_CLIENT_ID="your-client-id" export AZURE_CLIENT_SECRET="your-client-secret" CoseSignTool sign --p payload.txt --sf signature.cose \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile + --cp azure-artifact-signing \ + --aas-endpoint https://contoso.codesigning.azure.net \ + --aas-account-name ContosoAccount \ + --aas-cert-profile-name ContosoProfile ``` ```bash # With SCITT compliance and embedded payload CoseSignTool sign --p payload.txt --sf payload.cose --ep \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile \ + --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 "exp:2025-12-31T23:59:59Z" ``` @@ -331,10 +331,10 @@ CoseSignTool sign --p payload.txt --sf payload.cose --ep \ ```bash # Batch signing with piped input cat payload.txt | CoseSignTool sign --po \ - --cp azure-trusted-signing \ - --ats-endpoint https://contoso.codesigning.azure.net \ - --ats-account-name ContosoAccount \ - --ats-cert-profile-name ContosoProfile > signature.cose + --cp azure-artifact-signing \ + --aas-endpoint https://contoso.codesigning.azure.net \ + --aas-account-name ContosoAccount \ + --aas-cert-profile-name ContosoProfile > signature.cose ``` ### Creating Custom Certificate Providers