Skip to content

Commit 7ff6478

Browse files
github-actions[bot]Copilotmmitche
authored
[release/10.0] Enable generic detached signature support in SignTool (#16200)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Matt Mitchell (.NET) <mmitche@microsoft.com> Co-authored-by: mmitche <8725170+mmitche@users.noreply.github.com>
1 parent 33ebf26 commit 7ff6478

File tree

11 files changed

+293
-37
lines changed

11 files changed

+293
-37
lines changed

Arcade.slnx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<Project Path="src/Microsoft.DotNet.Arcade.Sdk/Microsoft.DotNet.Arcade.Sdk.csproj" />
4646
<Project Path="src/Microsoft.DotNet.ArcadeAzureIntegration/Microsoft.DotNet.ArcadeAzureIntegration.csproj" />
4747
<Project Path="src/Microsoft.DotNet.ArcadeLogging/Microsoft.DotNet.ArcadeLogging.csproj" />
48+
<Project Path="src/Microsoft.DotNet.Baselines.Tasks/Microsoft.DotNet.Baselines.Tasks.csproj" />
4849
<Project Path="src/Microsoft.DotNet.Build.Manifest/Microsoft.DotNet.Build.Manifest.csproj" />
4950
<Project Path="src/Microsoft.DotNet.Build.Tasks.Archives/Microsoft.DotNet.Build.Tasks.Archives.csproj" />
5051
<Project Path="src/Microsoft.DotNet.Build.Tasks.Feed/Microsoft.DotNet.Build.Tasks.Feed.csproj" />
@@ -71,7 +72,6 @@
7172
<Project Path="src/Microsoft.DotNet.SourceBuild/tasks/Microsoft.DotNet.SourceBuild.Tasks.csproj" />
7273
<Project Path="src/Microsoft.DotNet.StrongName/Microsoft.DotNet.StrongName.csproj" />
7374
<Project Path="src/Microsoft.DotNet.Tar/Microsoft.DotNet.Tar.csproj" />
74-
<Project Path="src/Microsoft.DotNet.Baselines.Tasks/Microsoft.DotNet.Baselines.Tasks.csproj" />
7575
<Project Path="src/Microsoft.DotNet.XliffTasks/Microsoft.DotNet.XliffTasks.csproj" />
7676
<Project Path="src/Microsoft.DotNet.XUnitAssert/src/Microsoft.DotNet.XUnitAssert.csproj" />
7777
<Project Path="src/Microsoft.DotNet.XUnitConsoleRunner/src/Microsoft.DotNet.XUnitConsoleRunner.csproj" />
17.3 KB
Binary file not shown.

src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,93 @@ public void SignZipFile()
12781278
});
12791279
}
12801280

1281+
[Fact]
1282+
public void SignArchivesUsingDetachedSignature()
1283+
{
1284+
// List of files to be considered for signing
1285+
var itemsToSign = new List<ItemToSign>()
1286+
{
1287+
new ItemToSign(GetResourcePath("test.zip")),
1288+
new ItemToSign(GetResourcePath("test.tgz")),
1289+
new ItemToSign(GetResourcePath("NestedZip.zip")),
1290+
new ItemToSign(GetResourcePath("InnerZipFile.zip"))
1291+
};
1292+
1293+
var strongNameSignInfo = new Dictionary<string, List<SignInfo>>();
1294+
1295+
// Overriding information
1296+
var explicitCertKeys = new Dictionary<ExplicitCertificateKey, string>()
1297+
{
1298+
{ new ExplicitCertificateKey("test.zip"), "ArchiveCert" },
1299+
{ new ExplicitCertificateKey("test.tgz"), "ArchiveCert" },
1300+
{ new ExplicitCertificateKey("InnerZipFile.zip"), "ArchiveCert" }
1301+
};
1302+
1303+
var additionalCertificateInfo = new Dictionary<string, List<AdditionalCertificateInformation>>()
1304+
{
1305+
{ "ArchiveCert",
1306+
new List<AdditionalCertificateInformation>() {
1307+
new AdditionalCertificateInformation() { GeneratesDetachedSignature = true }
1308+
}
1309+
}
1310+
};
1311+
1312+
ValidateFileSignInfos(itemsToSign, strongNameSignInfo, explicitCertKeys, s_fileExtensionSignInfo, new[]
1313+
{
1314+
"File 'NativeLibrary.dll' Certificate='Microsoft400'",
1315+
"File 'SOS.NETCore.dll' TargetFramework='.NETCoreApp,Version=v1.0' Certificate='Microsoft400'",
1316+
"File 'Nested.NativeLibrary.dll' Certificate='Microsoft400'",
1317+
"File 'Nested.SOS.NETCore.dll' TargetFramework='.NETCoreApp,Version=v1.0' Certificate='Microsoft400'",
1318+
"File 'test.zip' Certificate='ArchiveCert'",
1319+
"File 'test.tgz' Certificate='ArchiveCert'",
1320+
"File 'InnerZipFile.zip' Certificate='ArchiveCert'",
1321+
"File 'Mid.SOS.NETCore.dll' TargetFramework='.NETCoreApp,Version=v1.0' Certificate='Microsoft400'",
1322+
"File 'MidNativeLibrary.dll' Certificate='Microsoft400'",
1323+
"File 'NestedZip.zip'",
1324+
},
1325+
additionalCertificateInfo: additionalCertificateInfo,
1326+
expectedCopyFiles: new[]
1327+
{
1328+
$"{Path.Combine(_tmpDir, "ContainerSigning", "6", "InnerZipFile.zip")} -> {Path.Combine(_tmpDir, "InnerZipFile.zip")}",
1329+
$"{Path.Combine(_tmpDir, "ContainerSigning", "6", "InnerZipFile.zip.sig")} -> {Path.Combine(_tmpDir, "InnerZipFile.zip.sig")}"
1330+
});
1331+
1332+
ValidateGeneratedProject(itemsToSign, strongNameSignInfo, explicitCertKeys, s_fileExtensionSignInfo, new[]
1333+
{
1334+
$@"
1335+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "0", "NativeLibrary.dll"))}"">
1336+
<Authenticode>Microsoft400</Authenticode>
1337+
</FilesToSign>
1338+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "1", "SOS.NETCore.dll"))}"">
1339+
<Authenticode>Microsoft400</Authenticode>
1340+
</FilesToSign>
1341+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "2", "this_is_a_big_folder_name_look/this_is_an_even_more_longer_folder_name/but_this_one_is_ever_longer_than_the_previous_other_two/Nested.NativeLibrary.dll"))}"">
1342+
<Authenticode>Microsoft400</Authenticode>
1343+
</FilesToSign>
1344+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "3", "this_is_a_big_folder_name_look/this_is_an_even_more_longer_folder_name/but_this_one_is_ever_longer_than_the_previous_other_two/Nested.SOS.NETCore.dll"))}"">
1345+
<Authenticode>Microsoft400</Authenticode>
1346+
</FilesToSign>
1347+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "7", "Mid.SOS.NETCore.dll"))}"">
1348+
<Authenticode>Microsoft400</Authenticode>
1349+
</FilesToSign>
1350+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "8", "MidNativeLibrary.dll"))}"">
1351+
<Authenticode>Microsoft400</Authenticode>
1352+
</FilesToSign>
1353+
",
1354+
$@"
1355+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "test.zip"))}"">
1356+
<Authenticode>ArchiveCert</Authenticode>
1357+
</FilesToSign>
1358+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "test.tgz"))}"">
1359+
<Authenticode>ArchiveCert</Authenticode>
1360+
</FilesToSign>
1361+
<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "6", "InnerZipFile.zip"))}"">
1362+
<Authenticode>ArchiveCert</Authenticode>
1363+
</FilesToSign>
1364+
"
1365+
}, additionalCertificateInfo: additionalCertificateInfo);
1366+
}
1367+
12811368
/// <summary>
12821369
/// Verifies that signing of pkgs can be done on Windows, even though
12831370
/// we will not unpack or repack them.
@@ -2587,6 +2674,11 @@ public void ValidateSignToolTaskParsing()
25872674
}),
25882675
// Signed pe file
25892676
new TaskItem(GetResourcePath("SignedLibrary.dll"), new Dictionary<string, string>
2677+
{
2678+
{ SignToolConstants.CollisionPriorityId, "123" }
2679+
}),
2680+
// Sign a test.zip
2681+
new TaskItem(GetResourcePath("test.zip"), new Dictionary<string, string>
25902682
{
25912683
{ SignToolConstants.CollisionPriorityId, "123" }
25922684
})
@@ -2618,6 +2710,11 @@ public void ValidateSignToolTaskParsing()
26182710
{ "CertificateName", "DualSignCertificate" },
26192711
{ "PublicKeyToken", "31bf3856ad364e35" },
26202712
{ "CollisionPriorityId", "123" }
2713+
}),
2714+
new TaskItem("test.zip", new Dictionary<string, string>
2715+
{
2716+
{ "CertificateName", "DetachedArchiveCert" },
2717+
{ "CollisionPriorityId", "123" }
26212718
})
26222719
};
26232720

@@ -2634,7 +2731,11 @@ public void ValidateSignToolTaskParsing()
26342731
{ "MacCertificate", "MacDeveloperHarden" },
26352732
{ "MacNotarizationAppName", "com.microsoft.dotnet" },
26362733
{ "CollisionPriorityId", "123" }
2637-
})
2734+
}),
2735+
new TaskItem("DetachedArchiveCert", new Dictionary<string, string>
2736+
{
2737+
{ "DetachedSignature", "true" }
2738+
}),
26382739
};
26392740

26402741
var task = new SignToolTask
@@ -2667,7 +2768,11 @@ public void ValidateSignToolTaskParsing()
26672768
"File 'ProjectOne.dll' TargetFramework='.NETCoreApp,Version=v2.1' Certificate='3PartySHA2' StrongName='ArcadeStrongTest'",
26682769
"File 'ProjectOne.dll' TargetFramework='.NETStandard,Version=v2.0' Certificate='OverrideCertificateName' StrongName='ArcadeStrongTest'",
26692770
"File 'ContainerOne.1.0.0.nupkg' Certificate='NuGet'",
2670-
"File 'SignedLibrary.dll' TargetFramework='.NETCoreApp,Version=v2.0' Certificate='DualSignCertificate'"
2771+
"File 'SignedLibrary.dll' TargetFramework='.NETCoreApp,Version=v2.0' Certificate='DualSignCertificate'",
2772+
"File 'SOS.NETCore.dll' TargetFramework='.NETCoreApp,Version=v1.0' Certificate='Microsoft400'",
2773+
"File 'Nested.NativeLibrary.dll' Certificate='Microsoft400'",
2774+
"File 'Nested.SOS.NETCore.dll' TargetFramework='.NETCoreApp,Version=v1.0' Certificate='Microsoft400'",
2775+
"File 'test.zip' Certificate='DetachedArchiveCert'"
26712776
};
26722777
task.ParsedSigningInput.FilesToSign.Select(f => f.ToString()).Should().BeEquivalentTo(expected);
26732778
}

src/Microsoft.DotNet.SignTool/src/AdditionalCertificateInformation.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public class AdditionalCertificateInformation
2121
/// If the certificate name represents a sign+notarize operation, this is the name of the notarize operation.
2222
/// </summary>
2323
public string MacNotarizationAppName { get; set; }
24+
/// <summary>
25+
/// If true, this certificate should generate detached signatures instead of in-place signing.
26+
/// </summary>
27+
public bool GeneratesDetachedSignature { get; set; }
2428
public string CollisionPriorityId { get; set; }
2529
}
2630
}

src/Microsoft.DotNet.SignTool/src/BatchSignUtil.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -554,16 +554,30 @@ private void VerifyCertificates(TaskLoggingHelper log)
554554
}
555555
else if (fileName.IsZip())
556556
{
557-
if (fileName.SignInfo.Certificate != null)
557+
// Zip files can't be signed without a detached signature. If a certificate is provided but the signature is not detached.
558+
if (!fileName.SignInfo.GeneratesDetachedSignature && fileName.SignInfo.Certificate != null)
558559
{
559-
log.LogError($"Zip {fileName} should not be signed with this certificate: {fileName.SignInfo.Certificate}");
560+
log.LogError($"'{fileName}' may only be signed with a detached signature. '{fileName.SignInfo.Certificate}' does not produce a detached signature");
560561
}
561562

562563
if (fileName.SignInfo.StrongName != null)
563564
{
564565
log.LogError($"Zip {fileName} cannot be strong name signed.");
565566
}
566567
}
568+
else if (fileName.IsTarGZip())
569+
{
570+
// Tar.gz files can't be signed without a detached signature. If a certificate is provided but the signature is not detached.
571+
if (!fileName.SignInfo.GeneratesDetachedSignature && fileName.SignInfo.Certificate != null)
572+
{
573+
log.LogError($"'{fileName}' may only be signed with a detached signature. '{fileName.SignInfo.Certificate}' does not produce a detached signature");
574+
}
575+
576+
if (fileName.SignInfo.StrongName != null)
577+
{
578+
log.LogError($"TarGZip {fileName} cannot be strong name signed.");
579+
}
580+
}
567581
if (fileName.IsExecutableWixContainer())
568582
{
569583
if (isInvalidEmptyCertificate)
@@ -589,7 +603,28 @@ private void VerifyAfterSign(TaskLoggingHelper log, FileSignInfo file)
589603
// No need to check if the file should not have been signed.
590604
if (file.SignInfo.ShouldSign)
591605
{
592-
if (file.IsPEFile())
606+
// For files with detached signatures, verify the .sig file exists
607+
if (file.SignInfo.GeneratesDetachedSignature)
608+
{
609+
string sigFilePath = file.DetachedSignatureFullPath;
610+
if (!File.Exists(sigFilePath))
611+
{
612+
_log.LogError($"Detached signature file {sigFilePath} does not exist for {file.FullPath}");
613+
}
614+
else
615+
{
616+
var fileInfo = new FileInfo(sigFilePath);
617+
if (fileInfo.Length == 0)
618+
{
619+
_log.LogError($"Detached signature file {sigFilePath} is empty.");
620+
}
621+
else
622+
{
623+
_log.LogMessage(MessageImportance.Low, $"Detached signature file {sigFilePath} exists and is non-empty.");
624+
}
625+
}
626+
}
627+
else if (file.IsPEFile())
593628
{
594629
using (var stream = File.OpenRead(file.FullPath))
595630
{

src/Microsoft.DotNet.SignTool/src/Configuration.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ private FileSignInfo TrackFile(PathWithHash file, PathWithHash parentContainer,
224224

225225
// Copy the signed content to the destination path.
226226
_filesToCopy.Add(new KeyValuePair<string, string>(existingSignInfo.FullPath, file.FullPath));
227+
228+
// If this is a top-level file that uses detached signatures, also copy the detached signature file
229+
if (existingSignInfo.SignInfo.GeneratesDetachedSignature)
230+
{
231+
_filesToCopy.Add(new KeyValuePair<string, string>(existingSignInfo.DetachedSignatureFullPath, fileSignInfo.DetachedSignatureFullPath));
232+
_log.LogMessage(MessageImportance.Low, $"Will copy detached signature from '{existingSignInfo.DetachedSignatureFullPath}' to '{fileSignInfo.DetachedSignatureFullPath}'");
233+
}
234+
227235
return fileSignInfo;
228236
}
229237

@@ -262,7 +270,7 @@ private FileSignInfo TrackFile(PathWithHash file, PathWithHash parentContainer,
262270
// Only sign containers if the file itself is unsigned, or
263271
// an item in the container is unsigned.
264272
hasSignableParts = _zipDataMap[fileSignInfo.FileContentKey].NestedParts.Values.Any(b => b.FileSignInfo.SignInfo.ShouldSign || b.FileSignInfo.HasSignableParts);
265-
if(hasSignableParts)
273+
if (hasSignableParts)
266274
{
267275
// If the file has contents that need to be signed, then re-evaluate the signing info
268276
fileSignInfo = fileSignInfo.WithSignableParts();
@@ -529,6 +537,12 @@ private FileSignInfo ExtractSignInfo(
529537

530538
Check3rdPartyMicrosoftSignatureMismatch(file, peInfo, signInfo);
531539

540+
// Check if this cert should use detached signatures instead of in-place signing
541+
if (ShouldUseDetachedSignature(file, signInfo))
542+
{
543+
signInfo = signInfo.WithDetachedSignature(signInfo.Certificate);
544+
}
545+
532546
return new FileSignInfo(file, signInfo, (peInfo != null && peInfo.TargetFramework != "") ? peInfo.TargetFramework : null, wixContentFilePath: wixContentFilePath);
533547
}
534548

@@ -863,5 +877,27 @@ private bool ShouldSkip3rdPartyCheck(string fileName)
863877
{
864878
return _itemsToSkip3rdPartyCheck != null && _itemsToSkip3rdPartyCheck.Contains(Path.GetFileName(fileName));
865879
}
880+
881+
/// <summary>
882+
/// Determines if a file should use detached signatures based on certificate configuration.
883+
/// </summary>
884+
/// <param name="file">The file to check</param>
885+
/// <returns>True if the file should use detached signatures</returns>
886+
private bool ShouldUseDetachedSignature(PathWithHash file, SignInfo signInfo)
887+
{
888+
// Check if the certificate is configured for detached signatures
889+
if (signInfo.Certificate != null && _additionalCertificateInformation.TryGetValue(signInfo.Certificate, out var additionalInfo))
890+
{
891+
var additionalCertInfo = additionalInfo.FirstOrDefault(a => string.IsNullOrEmpty(a.CollisionPriorityId) ||
892+
a.CollisionPriorityId == signInfo.CollisionPriorityId);
893+
if (additionalCertInfo != null && additionalCertInfo.GeneratesDetachedSignature)
894+
{
895+
_log.LogMessage(MessageImportance.Low, $"File {file.FileName} will use detached signatures based on certificate configuration");
896+
return true;
897+
}
898+
}
899+
900+
return false;
901+
}
866902
}
867903
}

src/Microsoft.DotNet.SignTool/src/ExplicitCertificateKey.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,6 @@ public ExplicitCertificateKey(string fileName, string publicKeyToken = null, str
2525
ExecutableType = executableType;
2626
}
2727

28-
private static ExecutableType ParseExecutableType(string executableType)
29-
{
30-
if (string.IsNullOrEmpty(executableType))
31-
return ExecutableType.None;
32-
33-
return executableType switch
34-
{
35-
"PE" => ExecutableType.PE,
36-
"MachO" => ExecutableType.MachO,
37-
"ELF" => ExecutableType.ELF,
38-
_ => ExecutableType.None
39-
};
40-
}
41-
4228
public override bool Equals(object obj)
4329
=> obj is ExplicitCertificateKey key && Equals(key);
4430

src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ internal readonly struct FileSignInfo
1717
internal readonly SignInfo SignInfo;
1818
internal ImmutableArray<byte> ContentHash => File.ContentHash;
1919
internal readonly string WixContentFilePath;
20+
internal string DetachedSignatureFilePath => $"{FileName}.sig";
21+
internal string DetachedSignatureFullPath => $"{FullPath}.sig";
22+
2023
internal readonly PathWithHash File;
2124

2225
// optional file information that allows to disambiguate among multiple files with the same name:

0 commit comments

Comments
 (0)