diff --git a/eng/Signing.props b/eng/Signing.props
index 249203f1fee..78734d6e53d 100644
--- a/eng/Signing.props
+++ b/eng/Signing.props
@@ -69,12 +69,14 @@
+
+
diff --git a/tests/Infrastructure.Tests/Pipelines/NpmCliPackageTests.cs b/tests/Infrastructure.Tests/Pipelines/NpmCliPackageTests.cs
index 34ed923b48f..156f663f0f7 100644
--- a/tests/Infrastructure.Tests/Pipelines/NpmCliPackageTests.cs
+++ b/tests/Infrastructure.Tests/Pipelines/NpmCliPackageTests.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Xml.Linq;
using Xunit;
namespace Infrastructure.Tests;
@@ -200,6 +201,23 @@ public async Task NpmInstallValidationJobsUseExplicitJobsSharedStepsTemplateAndC
Assert.Contains("$(NPM_VALIDATION_SUMMARY_OSX_ARTIFACT)", releasePipeline);
}
+ [Fact]
+ public async Task NpmSigningScopeCoversNestedTarballPayloads()
+ {
+ var signingProps = XDocument.Parse(await ReadRepoFileAsync("eng/Signing.props"));
+
+ AssertScopedSigningRule(signingProps, "FileExtensionSignInfo", ".tgz", "LinuxSign500180PGP");
+ AssertScopedSigningRule(signingProps, "FileSignInfo", "aspire.js", "MicrosoftDotNet500");
+
+ // The native npm packages are built from already-signed native archives.
+ // The main Windows build should only produce the detached npm tarball
+ // signature; it must still provide scoped rules for nested native
+ // executables because Arcade resolves nested file certificates inside
+ // the ItemsToSign collision scope.
+ AssertScopedSigningRule(signingProps, "FileSignInfo", "aspire.exe", "None");
+ AssertScopedSigningRule(signingProps, "FileSignInfo", "aspire", "None");
+ }
+
[Fact]
public async Task ReleasePipelinePreflightsScheduledNpmPackagesBeforePublishing()
{
@@ -265,6 +283,21 @@ private static int CountOccurrences(string value, string substring)
return count;
}
+ private static void AssertScopedSigningRule(XDocument document, string elementName, string include, string certificateName)
+ {
+ var matchingRules = document
+ .Descendants(elementName)
+ .Where(element =>
+ (string?)element.Attribute("CollisionPriorityId") == "AspireCliNpmPackage" &&
+ ((string?)element.Attribute("Include") == include || (string?)element.Attribute("Update") == include) &&
+ (string?)element.Attribute("CertificateName") == certificateName)
+ .ToArray();
+
+ Assert.True(
+ matchingRules.Length == 1,
+ $"Expected exactly one {elementName} for '{include}' using '{certificateName}' in the AspireCliNpmPackage signing scope, but found {matchingRules.Length}.");
+ }
+
private static string FindRepoRoot()
{
string? current = AppContext.BaseDirectory;