Skip to content
Merged
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
272 changes: 174 additions & 98 deletions src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs

Large diffs are not rendered by default.

96 changes: 61 additions & 35 deletions src/Microsoft.DotNet.SignTool/src/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ internal class Configuration
private readonly string _pathToContainerUnpackingDirectory;

/// <summary>
/// This enable the overriding of the default certificate for a given file+token+target_framework.
/// It also contains a SignToolConstants.IgnoreFileCertificateSentinel flag in the certificate name in case the file does not need to be signed
/// for that
/// This enables the overriding of the default certificate for a given file+token+target_framework.
/// It also contains a SignToolConstants.IgnoreFileCertificateSentinel flag in the certificate name in case the file does not need to be signed.
/// Additionally, this can specify DoNotUnpack behavior for files without requiring a certificate to be specified.
/// </summary>
private readonly Dictionary<ExplicitCertificateKey, string> _fileSignInfo;
private readonly Dictionary<ExplicitSignInfoKey, FileSignInfoEntry> _fileSignInfo;

/// <summary>
/// Used to look for signing information when we have the PublicKeyToken of a file.
Expand All @@ -54,9 +54,9 @@ internal class Configuration
private List<WixPackInfo> _wixPacks;

/// <summary>
/// Mapping of ".ext" to certificate. Files that have an extension on this map
/// will be signed using the specified certificate. Input list might contain
/// duplicate entries
/// Mapping of ".ext" to signing information including certificate and DoNotUnpack flag.
/// Files that have an extension on this map will be signed using the specified certificate
/// and/or have DoNotUnpack behavior applied. Input list might contain duplicate entries.
/// </summary>
private readonly Dictionary<string, List<SignInfo>> _fileExtensionSignInfo;

Expand Down Expand Up @@ -111,7 +111,7 @@ public Configuration(
string tempDir,
List<ItemToSign> itemsToSign,
Dictionary<string, List<SignInfo>> strongNameInfo,
Dictionary<ExplicitCertificateKey, string> fileSignInfo,
Dictionary<ExplicitSignInfoKey, FileSignInfoEntry> fileSignInfo,
Dictionary<string, List<SignInfo>> extensionSignInfo,
Dictionary<string, List<AdditionalCertificateInformation>> additionalCertificateInformation,
HashSet<string> itemsToSkip3rdPartyCheck,
Expand Down Expand Up @@ -235,37 +235,47 @@ private FileSignInfo TrackFile(PathWithHash file, PathWithHash parentContainer,
return fileSignInfo;
}

// Skip unpacking if DoNotUnpack is set on the SignInfo (from FileSignInfo or FileExtensionSignInfo)
bool doNotUnpack = fileSignInfo.SignInfo.DoNotUnpack;
if (fileSignInfo.IsUnpackableContainer())
{
if (fileSignInfo.IsUnpackableWixContainer())
if (doNotUnpack)
{
_log.LogMessage($"Trying to gather data for wix container {fileSignInfo.FullPath}");
if (TryBuildWixData(fileSignInfo, out var msiData))
{
_zipDataMap[fileSignInfo.FileContentKey] = msiData;
}
else
{
_log.LogError($"Failed to build wix data for {fileSignInfo.FullPath}");
}
_log.LogMessage(MessageImportance.Normal, "Skipping container unpacking for '{0}' due to DoNotUnpack flag", file.FullPath);
}
else
{
if (TryBuildZipData(fileSignInfo, out var zipData))
if (fileSignInfo.IsUnpackableWixContainer())
{
_zipDataMap[fileSignInfo.FileContentKey] = zipData;
_log.LogMessage($"Trying to gather data for wix container {fileSignInfo.FullPath}");
if (TryBuildWixData(fileSignInfo, out var msiData))
{
_zipDataMap[fileSignInfo.FileContentKey] = msiData;
}
else
{
_log.LogError($"Failed to build wix data for {fileSignInfo.FullPath}");
}
}
else
{
_log.LogError($"Failed to build zip data for {fileSignInfo.FullPath}");
if (TryBuildZipData(fileSignInfo, out var zipData))
{
_zipDataMap[fileSignInfo.FileContentKey] = zipData;
}
else
{
_log.LogError($"Failed to build zip data for {fileSignInfo.FullPath}");
}
}
}
}

_log.LogMessage(MessageImportance.Low, $"Caching file {fileSignInfo.FileContentKey.FileName} {fileSignInfo.FileContentKey.StringHash}");
_filesByContentKey.Add(fileSignInfo.FileContentKey, fileSignInfo);

bool hasSignableParts = false;
if (fileSignInfo.IsUnpackableContainer())
if (fileSignInfo.IsUnpackableContainer() && !doNotUnpack)
{
// Only sign containers if the file itself is unsigned, or
// an item in the container is unsigned.
Expand Down Expand Up @@ -306,7 +316,7 @@ private FileSignInfo ExtractSignInfo(
string wixContentFilePath)
{
var extension = Path.GetExtension(file.FileName);
string explicitCertificateName = null;
FileSignInfoEntry explicitFileSignInfoEntry = null;
var fileSpec = string.Empty;
var isAlreadyAuthenticodeSigned = false;
var isAlreadyStrongNamed = false;
Expand Down Expand Up @@ -426,12 +436,12 @@ private FileSignInfo ExtractSignInfo(

// Check if we have more specific sign info:
matchedNameTokenFramework = _fileSignInfo.TryGetValue(
new ExplicitCertificateKey(file.FileName, peInfo.PublicKeyToken, peInfo.TargetFramework, _hashToCollisionIdMap[signedFileContentKey]),
out explicitCertificateName);
new ExplicitSignInfoKey(file.FileName, peInfo.PublicKeyToken, peInfo.TargetFramework, _hashToCollisionIdMap[signedFileContentKey]),
out explicitFileSignInfoEntry);

matchedNameToken = !matchedNameTokenFramework && _fileSignInfo.TryGetValue(
new ExplicitCertificateKey(file.FileName, peInfo.PublicKeyToken, collisionPriorityId: _hashToCollisionIdMap[signedFileContentKey]),
out explicitCertificateName);
new ExplicitSignInfoKey(file.FileName, peInfo.PublicKeyToken, collisionPriorityId: _hashToCollisionIdMap[signedFileContentKey]),
out explicitFileSignInfoEntry);

fileSpec = matchedNameTokenFramework ? $" (PublicKeyToken = {peInfo.PublicKeyToken}, Framework = {peInfo.TargetFramework})" :
matchedNameToken ? $" (PublicKeyToken = {peInfo.PublicKeyToken})" : string.Empty;
Expand Down Expand Up @@ -462,29 +472,39 @@ private FileSignInfo ExtractSignInfo(
}

// We didn't find any specific information for PE files using PKT + TargetFramework
if (explicitCertificateName == null)
if (explicitFileSignInfoEntry == null)
{
// First try with ExecutableType
var matchedNameAndExecutableType = _fileSignInfo.TryGetValue(new ExplicitCertificateKey(file.FileName,
collisionPriorityId: _hashToCollisionIdMap[signedFileContentKey], executableType: executableType), out explicitCertificateName);
var matchedNameAndExecutableType = _fileSignInfo.TryGetValue(new ExplicitSignInfoKey(file.FileName,
collisionPriorityId: _hashToCollisionIdMap[signedFileContentKey], executableType: executableType), out explicitFileSignInfoEntry);

// If no match with ExecutableType, try without it for backward compatibility
if (!matchedNameAndExecutableType)
{
matchedName = _fileSignInfo.TryGetValue(new ExplicitCertificateKey(file.FileName,
collisionPriorityId: _hashToCollisionIdMap[signedFileContentKey]), out explicitCertificateName);
matchedName = _fileSignInfo.TryGetValue(new ExplicitSignInfoKey(file.FileName,
collisionPriorityId: _hashToCollisionIdMap[signedFileContentKey]), out explicitFileSignInfoEntry);
}
else
{
matchedName = matchedNameAndExecutableType;
}
}

// Extract explicit certificate name and DoNotUnpack flag from FileSignInfoEntry
string explicitCertificateName = explicitFileSignInfoEntry?.CertificateName;

// Determine DoNotUnpack value:
// - If FileSignInfo is present, use its DoNotUnpack value (takes precedence)
// - Otherwise, use the DoNotUnpack from FileExtensionSignInfo (via signInfo)
bool doNotUnpack = explicitFileSignInfoEntry != null
? explicitFileSignInfoEntry.DoNotUnpack
: signInfo.DoNotUnpack;

// If has overriding info, is it for ignoring the file?
if (SignToolConstants.IgnoreFileCertificateSentinel.Equals(explicitCertificateName, StringComparison.OrdinalIgnoreCase))
{
_log.LogMessage(MessageImportance.Low, $"File configured to not be signed: {file.FullPath}{fileSpec}");
return new FileSignInfo(file, SignInfo.Ignore);
return new FileSignInfo(file, SignInfo.Ignore.WithDoNotUnpack(doNotUnpack));
}

// Do we have an explicit certificate after all?
Expand All @@ -493,6 +513,12 @@ private FileSignInfo ExtractSignInfo(
signInfo = signInfo.WithCertificateName(explicitCertificateName, _hashToCollisionIdMap[signedFileContentKey]);
hasSignInfo = true;
}

// Apply DoNotUnpack from FileSignInfo if present (takes precedence over extension-based DoNotUnpack)
if (explicitFileSignInfoEntry != null)
{
signInfo = signInfo.WithDoNotUnpack(explicitFileSignInfoEntry.DoNotUnpack);
}

if (hasSignInfo)
{
Expand All @@ -517,7 +543,7 @@ private FileSignInfo ExtractSignInfo(
_log.LogWarning($"Skipping file '{file.FullPath}' because .js files are no longer signed by default. " +
"To disable this warning, please explicitly define the FileExtensionSignInfo for the .js extension " +
"or set the MSBuild property 'NoSignJS' to 'true'.");
return new FileSignInfo(file, SignInfo.Ignore, wixContentFilePath: wixContentFilePath);
return new FileSignInfo(file, SignInfo.Ignore.WithDoNotUnpack(doNotUnpack), wixContentFilePath: wixContentFilePath);
}

// If the file is already signed and we are not allowed to dual sign, and we are not doing a mac notarization operation,
Expand Down Expand Up @@ -556,7 +582,7 @@ private FileSignInfo ExtractSignInfo(
_log.LogMessage(MessageImportance.Low, $"Ignoring non-signable file: {file.FullPath}");
}

return new FileSignInfo(file, SignInfo.Ignore, wixContentFilePath: wixContentFilePath);
return new FileSignInfo(file, SignInfo.Ignore.WithDoNotUnpack(doNotUnpack), wixContentFilePath: wixContentFilePath);

bool IsSigned(PathWithHash file, SigningStatus signingStatus)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@

namespace Microsoft.DotNet.SignTool
{
internal readonly struct ExplicitCertificateKey : IEquatable<ExplicitCertificateKey>
/// <summary>
/// Key used to identify file-specific signing information in FileSignInfo.
/// Represents the entity being signed (file name, path, attributes) rather than just certificate correlation.
/// Can be used to specify signing properties like DoNotUnpack independently of certificates.
/// </summary>
internal readonly struct ExplicitSignInfoKey : IEquatable<ExplicitSignInfoKey>
{
public readonly string FileName;
public readonly string PublicKeyToken;
public readonly string TargetFramework;
public readonly string CollisionPriorityId;
public readonly ExecutableType ExecutableType;

public ExplicitCertificateKey(string fileName, string publicKeyToken = null, string targetFramework = null, string collisionPriorityId = null, ExecutableType executableType = ExecutableType.None)
public ExplicitSignInfoKey(string fileName, string publicKeyToken = null, string targetFramework = null, string collisionPriorityId = null, ExecutableType executableType = ExecutableType.None)
{
Debug.Assert(fileName != null);

Expand All @@ -26,22 +31,22 @@ public ExplicitCertificateKey(string fileName, string publicKeyToken = null, str
}

public override bool Equals(object obj)
=> obj is ExplicitCertificateKey key && Equals(key);
=> obj is ExplicitSignInfoKey key && Equals(key);

public override int GetHashCode()
=> Hash.Combine(Hash.Combine(FileName.GetHashCode(), PublicKeyToken.GetHashCode()), Hash.Combine(TargetFramework.GetHashCode(), ExecutableType.GetHashCode()));

bool IEquatable<ExplicitCertificateKey>.Equals(ExplicitCertificateKey other)
bool IEquatable<ExplicitSignInfoKey>.Equals(ExplicitSignInfoKey other)
=> FileName == other.FileName &&
CollisionPriorityId == other.CollisionPriorityId &&
string.Equals(PublicKeyToken, other.PublicKeyToken, StringComparison.OrdinalIgnoreCase) &&
TargetFramework == other.TargetFramework &&
ExecutableType == other.ExecutableType;

public static bool operator ==(ExplicitCertificateKey key1, ExplicitCertificateKey key2)
public static bool operator ==(ExplicitSignInfoKey key1, ExplicitSignInfoKey key2)
=> key1.Equals(key2);

public static bool operator !=(ExplicitCertificateKey key1, ExplicitCertificateKey key2)
public static bool operator !=(ExplicitSignInfoKey key1, ExplicitSignInfoKey key2)
=> !(key1 == key2);
}
}
5 changes: 3 additions & 2 deletions src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ internal bool IsUnpackableContainer() => IsZip() ||

internal bool HasSignableParts { get; }

internal bool ShouldRepack => HasSignableParts;
internal bool ShouldRepack => HasSignableParts && !SignInfo.DoNotUnpack;

internal bool ShouldTrack => SignInfo.ShouldSign || ShouldRepack;

Expand All @@ -150,7 +150,8 @@ public override string ToString()
(TargetFramework != null ? $" TargetFramework='{TargetFramework}'" : "") +
(SignInfo.ShouldSign ? $" Certificate='{SignInfo.Certificate}'" : "") +
(SignInfo.ShouldStrongName ? $" StrongName='{SignInfo.StrongName}'" : "") +
(SignInfo.ShouldNotarize ? $" NotarizationAppName='{SignInfo.NotarizationAppName}'" : "");
(SignInfo.ShouldNotarize ? $" NotarizationAppName='{SignInfo.NotarizationAppName}'" : "") +
(HasSignableParts && SignInfo.DoNotUnpack ? " DoNotUnpack='true'" : "");

internal FileSignInfo WithSignableParts()
=> new FileSignInfo(File, SignInfo.WithIsAlreadySigned(false), TargetFramework, WixContentFilePath, true);
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.DotNet.SignTool/src/FileSignInfoEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.SignTool
{
/// <summary>
/// Represents file signing information including certificate name and DoNotUnpack flag.
/// Certificate name can be null if only DoNotUnpack behavior needs to be specified.
/// </summary>
internal class FileSignInfoEntry
{
public string CertificateName { get; }
public bool DoNotUnpack { get; }

public FileSignInfoEntry(string certificateName = null, bool doNotUnpack = false)
{
CertificateName = certificateName;
DoNotUnpack = doNotUnpack;
}
}
}
Loading
Loading