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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/Sign.Core/DataFormatSigners/AzureSignToolSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE.txt file in the project root for more information.

using AzureSign.Core;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using AzureSign.Core;
using Microsoft.Extensions.Logging;

namespace Sign.Core
{
Expand Down Expand Up @@ -119,12 +119,16 @@ public async Task SignAsync(IEnumerable<FileInfo> files, SignOptions options)

using (X509Certificate2 certificate = await _certificateProvider.GetCertificateAsync())
using (RSA rsa = await _signatureAlgorithmProvider.GetRsaAsync())
using (AuthenticodeKeyVaultSigner signer = new(
rsa,
certificate,
options.FileHashAlgorithm,
timestampConfiguration))
{
X509Certificate2Collection additionalCertificates = await _certificateProvider.GetAdditionalCertificatesAsync();

using AuthenticodeKeyVaultSigner signer = new(
rsa,
certificate,
options.FileHashAlgorithm,
timestampConfiguration,
additionalCertificates);

// Partition files: STA-required files (.js, .vbs) are signed sequentially
// to avoid blocking ThreadPool threads (each STA call uses thread.Join()).
// Non-STA files are signed in parallel as before.
Expand Down
7 changes: 7 additions & 0 deletions src/Sign.Core/ICertificateProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@ internal interface ICertificateProvider
/// </summary>
/// <returns>An <see cref="X509Certificate2"/> certificate acquired from the initialized certificate service.</returns>
Task<X509Certificate2> GetCertificateAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Acquires additional certificates (e.g. intermediates) used to build the chain for the signing certificate.
/// Providers that do not have a chain to expose return an empty collection.
/// </summary>
Task<X509Certificate2Collection> GetAdditionalCertificatesAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new X509Certificate2Collection());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE.txt file in the project root for more information.

using System.Diagnostics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Azure;
using Azure.CodeSigning;
using Microsoft.Extensions.Logging;
using Sign.Core;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Sign.SignatureProviders.ArtifactSigning
{
Expand All @@ -20,6 +20,7 @@ internal sealed class ArtifactSigningService : ISignatureAlgorithmProvider, ICer
private readonly ILogger<ArtifactSigningService> _logger;
private readonly SemaphoreSlim _mutex = new(1);
private X509Certificate2? _certificate;
private X509Certificate2Collection? _additionalCertificates;

public ArtifactSigningService(
CertificateProfileClient certificateProfileClient,
Expand Down Expand Up @@ -72,8 +73,12 @@ public async Task<X509Certificate2> GetCertificateAsync(CancellationToken cancel
X509Certificate2Collection collection = [];
collection.Import(rawData);

// This should contain the certificate chain in root->leaf order.
_certificate = collection[collection.Count - 1];
// Find the leaf: the cert whose Subject is not any other cert's Issuer.
IEnumerable<string> issuers = collection.Cast<X509Certificate2>().Select(x => x.Issuer);
_certificate = collection.Cast<X509Certificate2>().FirstOrDefault(x => !issuers.Contains(x.Subject))
?? throw new InvalidOperationException("Unable to locate leaf certificate");

_additionalCertificates = [.. collection.Cast<X509Certificate2>().Where(x => x != _certificate).ToArray()];

_logger.LogTrace(Resources.FetchedCertificate, stopwatch.Elapsed.TotalMilliseconds);
//print the certificate info
Expand All @@ -95,5 +100,15 @@ public async Task<RSA> GetRsaAsync(CancellationToken cancellationToken)
RSA rsaPublicKey = certificate.GetRSAPublicKey()!;
return new RSAArtifactSigning(_client, _accountName, _certificateProfileName, rsaPublicKey);
}

public async Task<X509Certificate2Collection> GetAdditionalCertificatesAsync(CancellationToken cancellationToken = default)
{
if (_additionalCertificates is null)
{
await GetCertificateAsync(cancellationToken);
}

return _additionalCertificates ?? [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RS
SignRequest request = new(signatureAlgorithm, hash);
CertificateProfileSignOperation operation = _client.StartSign(_accountName, _certificateProfileName, request);
Response<SignStatus> response = operation.WaitForCompletion();
return response.Value.Signature;
byte[] signature = response.Value.Signature;

if (!_rsaPublicKey.VerifyHash(hash, signature, hashAlgorithm, padding))
{
throw new CryptographicException("Invalid signature");
}

return signature;
}

public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE.txt file in the project root for more information.

using Azure;
using Azure.CodeSigning;
using Microsoft.Extensions.Logging;
using Moq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Sign.SignatureProviders.ArtifactSigning.Test
{
public class ArtifactSigningServiceTests
{
private static readonly CertificateProfileClient CertificateProfileClient = Mock.Of<CertificateProfileClient>();
private const string AccountName = "a";
private const string CertificateProfileName = "b";
private static readonly ILogger<ArtifactSigningService> Logger = Mock.Of<ILogger<ArtifactSigningService>>();

[Fact]
public void Constructor_WhenCertificateProfileClientIsNull_Throws()
{
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(
() => new ArtifactSigningService(certificateProfileClient: null!, AccountName, CertificateProfileName, Logger));

Assert.Equal("certificateProfileClient", exception.ParamName);
}

[Fact]
public void Constructor_WhenAccountNameIsNull_Throws()
{
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(
() => new ArtifactSigningService(CertificateProfileClient, accountName: null!, CertificateProfileName, Logger));

Assert.Equal("accountName", exception.ParamName);
}

[Fact]
public void Constructor_WhenAccountNameIsEmpty_Throws()
{
ArgumentException exception = Assert.Throws<ArgumentException>(
() => new ArtifactSigningService(CertificateProfileClient, accountName: string.Empty, CertificateProfileName, Logger));

Assert.Equal("accountName", exception.ParamName);
}

[Fact]
public void Constructor_WhenCertificateProfileNameIsNull_Throws()
{
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(
() => new ArtifactSigningService(CertificateProfileClient, AccountName, certificateProfileName: null!, Logger));

Assert.Equal("certificateProfileName", exception.ParamName);
}

[Fact]
public void Constructor_WhenCertificateProfileNameIsEmpty_Throws()
{
ArgumentException exception = Assert.Throws<ArgumentException>(
() => new ArtifactSigningService(CertificateProfileClient, AccountName, certificateProfileName: string.Empty, Logger));

Assert.Equal("certificateProfileName", exception.ParamName);
}

[Fact]
public void Constructor_WhenLoggerIsNull_Throws()
{
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(
() => new ArtifactSigningService(CertificateProfileClient, AccountName, CertificateProfileName, logger: null!));

Assert.Equal("logger", exception.ParamName);
}

[Fact]
public async Task GetCertificateAsync_WhenChainReturned_ReturnsLeafCertificate()
{
var (root, intermediate, leaf) = BuildCertificateChain();
using (root)
using (intermediate)
using (leaf)
{
Mock<CertificateProfileClient> client = CreateClientReturningChain(root, intermediate, leaf);

using ArtifactSigningService service = new(client.Object, AccountName, CertificateProfileName, Logger);

using X509Certificate2 result = await service.GetCertificateAsync(CancellationToken.None);

Assert.Equal(leaf.Thumbprint, result.Thumbprint);
}
}

[Fact]
public async Task GetAdditionalCertificatesAsync_WhenChainReturned_ReturnsNonLeafCertificates()
{
var (root, intermediate, leaf) = BuildCertificateChain();
using (root)
using (intermediate)
using (leaf)
{
Mock<CertificateProfileClient> client = CreateClientReturningChain(root, intermediate, leaf);

using ArtifactSigningService service = new(client.Object, AccountName, CertificateProfileName, Logger);

X509Certificate2Collection result = await service.GetAdditionalCertificatesAsync(CancellationToken.None);

Assert.Equal(2, result.Count);
HashSet<string> thumbprints = result.Cast<X509Certificate2>().Select(c => c.Thumbprint).ToHashSet();
Assert.Contains(root.Thumbprint, thumbprints);
Assert.Contains(intermediate.Thumbprint, thumbprints);
Assert.DoesNotContain(leaf.Thumbprint, thumbprints);
}
}

private static Mock<CertificateProfileClient> CreateClientReturningChain(params X509Certificate2[] certificates)
{
X509Certificate2Collection collection = new(certificates);
byte[] pkcs7 = collection.Export(X509ContentType.Pkcs7)!;

Mock<Response<Stream>> response = new();
response.SetupGet(_ => _.Value).Returns(new MemoryStream(pkcs7));

Mock<CertificateProfileClient> client = new();
client
.Setup(_ => _.GetSignCertificateChainAsync(AccountName, CertificateProfileName, It.IsAny<CancellationToken>()))
.ReturnsAsync(response.Object);

return client;
}

/// <summary>
/// Builds a certificate chain with a root, intermediate, and leaf certificate.
/// </summary>
private static (X509Certificate2 root, X509Certificate2 intermediate, X509Certificate2 leaf) BuildCertificateChain()
{
DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddDays(-1);
DateTimeOffset notAfter = DateTimeOffset.UtcNow.AddDays(1);

using RSA rootKey = RSA.Create(2048);
CertificateRequest rootRequest = new("CN=Test Root", rootKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
rootRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(certificateAuthority: true, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: true));
rootRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign, critical: true));
X509Certificate2 root = rootRequest.CreateSelfSigned(notBefore, notAfter);

using RSA intermediateKey = RSA.Create(2048);
CertificateRequest intermediateRequest = new("CN=Test Intermediate", intermediateKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
intermediateRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(certificateAuthority: true, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: true));
intermediateRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign, critical: true));
X509Certificate2 intermediateNoKey = intermediateRequest.Create(root, notBefore, notAfter, [1]);
X509Certificate2 intermediate = intermediateNoKey.CopyWithPrivateKey(intermediateKey);
intermediateNoKey.Dispose();

using RSA leafKey = RSA.Create(2048);
CertificateRequest leafRequest = new("CN=Test Leaf", leafKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
X509Certificate2 leafNoKey = leafRequest.Create(intermediate, notBefore, notAfter, [2]);
X509Certificate2 leaf = leafNoKey.CopyWithPrivateKey(leafKey);
leafNoKey.Dispose();

return (root, intermediate, leaf);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE.txt file in the project root for more information.

using System.Security.Cryptography;
using Azure;
using Azure.CodeSigning;
using Azure.CodeSigning.Models;
using Moq;
using Moq.Protected;
using Sign.SignatureProviders.ArtifactSigning;
using System.Security.Cryptography;

namespace Sign.SignatureProviders.KeyVault.Test
{
public class RSATrustedSigningTests
public class RSAArtifactSigningTests
{
private static readonly string AccountName = "testAccount";
private static readonly string CertificateProfileName = "testProfile";
Expand Down Expand Up @@ -161,6 +161,8 @@ public void SignHash_UsesClient(int hashLength, string paddingName, string expec
_client.Setup(_ => _.StartSign(AccountName, CertificateProfileName, It.IsAny<SignRequest>(), null, null, null, default))
.Returns(operation.Object);

_rsaPublicKey.Setup(_ => _.VerifyHash(hash, signature, hashAlgorithmName, padding)).Returns(true);

var result = rsa.SignHash(hash, hashAlgorithmName, padding);

Assert.Same(signature, result);
Expand Down

This file was deleted.