Skip to content

Commit

Permalink
Signcheck pkgs
Browse files Browse the repository at this point in the history
  • Loading branch information
ellahathaway committed Feb 14, 2025
1 parent aecf645 commit e266d5f
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<ExcludeFromSourceOnlyBuild>true</ExcludeFromSourceOnlyBuild>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Microsoft.DotNet.MacOsPkg\Core\Microsoft.DotNet.MacOsPkg.Core.csproj" Condition="'$(TargetFramework)' == '$(NetToolCurrent)'" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="LZMA-SDK" />
<PackageReference Include="Microsoft.VisualStudio.OLE.Interop" Condition="$(TargetFramework) != $(NetToolCurrent)"/>
Expand Down Expand Up @@ -57,6 +61,10 @@
<Compile Remove="Verification\Jar\JarSignatureFile.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != '$(NetToolCurrent)'">
<Compile Remove="Verification\PkgVerifier.cs" />
</ItemGroup>

<ItemGroup>
<Compile Update="SignCheckResources.Designer.cs"
DesignTime="True"
Expand Down
52 changes: 33 additions & 19 deletions src/SignCheck/Microsoft.SignCheck/Verification/ArchiveVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,47 @@ protected void VerifyContent(SignatureVerificationResult svr)
Log.WriteMessage(LogVerbosity.Diagnostic, SignCheckResources.DiagExtractingFileContents, tempPath);
Dictionary<string, string> archiveMap = new Dictionary<string, string>();

foreach (ArchiveEntry archiveEntry in ReadArchiveEntries(svr.FullPath))
try
{
string aliasFullName = GenerateArchiveEntryAlias(archiveEntry, tempPath);
if (File.Exists(aliasFullName))
foreach (ArchiveEntry archiveEntry in ReadArchiveEntries(svr.FullPath))
{
Log.WriteMessage(LogVerbosity.Normal, SignCheckResources.FileAlreadyExists, aliasFullName);
string aliasFullName = GenerateArchiveEntryAlias(archiveEntry, tempPath);
if (File.Exists(aliasFullName))
{
Log.WriteMessage(LogVerbosity.Normal, SignCheckResources.FileAlreadyExists, aliasFullName);
}
else
{
CreateDirectory(Path.GetDirectoryName(aliasFullName));
WriteArchiveEntry(archiveEntry, aliasFullName);
archiveMap[archiveEntry.RelativePath] = aliasFullName;
}
}
else

// We can only verify once everything is extracted. This is mainly because MSIs can have mutliple external CAB files
// and we need to ensure they are extracted before we verify the MSIs.
foreach (string fullName in archiveMap.Keys)
{
CreateDirectory(Path.GetDirectoryName(aliasFullName));
WriteArchiveEntry(archiveEntry, aliasFullName);
archiveMap[archiveEntry.RelativePath] = aliasFullName;
SignatureVerificationResult result = VerifyFile(archiveMap[fullName], svr.Filename,
Path.Combine(svr.VirtualPath, fullName), fullName);

// Tag the full path into the result detail
result.AddDetail(DetailKeys.File, SignCheckResources.DetailFullName, fullName);
svr.NestedResults.Add(result);
}
}

// We can only verify once everything is extracted. This is mainly because MSIs can have mutliple external CAB files
// and we need to ensure they are extracted before we verify the MSIs.
foreach (string fullName in archiveMap.Keys)
catch (PlatformNotSupportedException)
{
SignatureVerificationResult result = VerifyFile(archiveMap[fullName], svr.Filename,
Path.Combine(svr.VirtualPath, fullName), fullName);

// Tag the full path into the result detail
result.AddDetail(DetailKeys.File, SignCheckResources.DetailFullName, fullName);
svr.NestedResults.Add(result);
// Log the error and return an unsupported file type result
// because some archive types are not supported on all platforms
string parent = Path.GetDirectoryName(svr.FullPath) ?? SignCheckResources.NA;
svr = SignatureVerificationResult.UnsupportedFileTypeResult(svr.FullPath, parent, svr.VirtualPath);
svr.AddDetail(DetailKeys.File, SignCheckResources.DetailSigned, SignCheckResources.NA);
}
finally
{
DeleteDirectory(tempPath);
}
DeleteDirectory(tempPath);
}
}

Expand Down
193 changes: 193 additions & 0 deletions src/SignCheck/Microsoft.SignCheck/Verification/PkgVerifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.SignCheck.Logging;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;
using Microsoft.DotNet.MacOsPkg.Core;

namespace Microsoft.SignCheck.Verification
{
public class PkgVerifier : ArchiveVerifier
{
public PkgVerifier(Log log, Exclusions exclusions, SignatureVerificationOptions options, string fileExtension) : base(log, exclusions, options, fileExtension)
{
if (fileExtension != ".pkg" && fileExtension != ".app")
{
throw new ArgumentException("PkgVerifier can only be used with .pkg and .app files.");
}
}

public override SignatureVerificationResult VerifySignature(string path, string parent, string virtualPath)
{
SignatureVerificationResult svr = new SignatureVerificationResult(path, parent, virtualPath);
string fullPath = svr.FullPath;

try
{
svr.IsSigned = IsSigned(fullPath, svr);
svr.AddDetail(DetailKeys.File, SignCheckResources.DetailSigned, svr.IsSigned);
}
catch (PlatformNotSupportedException)
{
// Log the error and return an unsupported file type result
// because processing pkgs and apps is not supported on non-OSX platforms
svr = SignatureVerificationResult.UnsupportedFileTypeResult(path, parent, virtualPath);
svr.AddDetail(DetailKeys.File, SignCheckResources.DetailSigned, SignCheckResources.NA);
}

VerifyContent(svr);

return svr;
}

protected override IEnumerable<ArchiveEntry> ReadArchiveEntries(string archivePath)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
throw new PlatformNotSupportedException("The MacOsPkg tooling is only supported on macOS.");
}

string extractionPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
try
{
if (MacOsPkgCore.Unpack(archivePath, extractionPath) != 0)
{
throw new Exception($"Failed to unpack pkg '{archivePath}'");
}

foreach (var path in Directory.EnumerateFiles(extractionPath, "*.*", SearchOption.AllDirectories))
{
var relativePath = path.Substring(extractionPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/');
using var stream = (Stream)File.Open(path, FileMode.Open);
yield return new ArchiveEntry()
{
RelativePath = relativePath,
ContentStream = stream,
ContentSize = stream?.Length ?? 0
};
}
}
finally
{
// Cleanup the extraction path if it was created by the Unpack method
if (Directory.Exists(extractionPath))
{
Directory.Delete(extractionPath, true);
}
}
}

private bool IsSigned(string path, SignatureVerificationResult svr)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
throw new PlatformNotSupportedException("The MacOsPkg tooling is only supported on macOS.");
}

// macOsPkgCore.VerifySignature writes the output to the console.
// We need to capture the output and parse it for timestamps.
var consoleOutput = Console.Out;
StringWriter outputWriter = new StringWriter();
Console.SetOut(outputWriter);

var errorOutput = Console.Error;
StringWriter errorOutputWriter = new StringWriter();
Console.SetError(errorOutputWriter);

try
{
// Verify the signature
if (MacOsPkgCore.VerifySignature(path) != 0)
{
string errorMessage = errorOutputWriter.ToString();
// Ignore the pkgutil --check-signature error message, it is expected if the file is not signed
if (!string.IsNullOrEmpty(errorMessage) && !errorMessage.Contains("pkgutil --check-signature"))
{
svr.AddDetail(DetailKeys.Error, errorMessage);
}
return false;
}

// Verify the timestamps
IEnumerable<Timestamp> timestamps = GetTimestamps(outputWriter.ToString());
if (!timestamps.Any())
{
svr.AddDetail(DetailKeys.Error, SignCheckResources.ErrorInvalidOrMissingTimestamp);
return false;
}

foreach (Timestamp ts in timestamps)
{
if (ts.IsValid)
{
svr.AddDetail(DetailKeys.Misc, SignCheckResources.DetailTimestamp, ts.SignedOn, ts.SignatureAlgorithm);
}
else
{
if (ts.SignedOn == DateTime.MaxValue || ts.ExpiryDate == DateTime.MinValue)
{
svr.AddDetail(DetailKeys.Error, SignCheckResources.ErrorInvalidOrMissingTimestamp);
}
else
{
svr.AddDetail(DetailKeys.Error, SignCheckResources.DetailTimestampOutisdeCertValidity, ts.SignedOn, ts.EffectiveDate, ts.ExpiryDate);
}
return false;
}
}
return true;
}
finally
{
Console.SetOut(consoleOutput);
Console.SetError(errorOutput);
}
}

/// <summary>
/// Get the timestamps from the output of the pkgutil command.
/// Assumes that the verify command has already been run.
/// </summary>
private IEnumerable<Timestamp> GetTimestamps(string signingVerificationOutput)
{
string timestampRegex = @"(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \+\d{4})";

Regex signedOnRegex = new Regex(@"Signed with a trusted timestamp on: " + timestampRegex);
string signedOnString = GetRegexValue(signedOnRegex.Match(signingVerificationOutput), "timestamp");
if(!DateTime.TryParse(signedOnString, out DateTime signedOnTimestamp))
{
signedOnTimestamp = DateTime.MaxValue;
}

Regex certificateChainRegex = new Regex(@"Expires: " + timestampRegex + "\n (?<algorithm>.+) Fingerprint:");
IEnumerable<Match> matches = certificateChainRegex.Matches(signingVerificationOutput).ToList();

return matches.Select(match =>
{
string certificateString = GetRegexValue(match, "timestamp");
if (!DateTime.TryParse(certificateString, out DateTime certificateTimestamp))
{
certificateTimestamp = DateTime.MinValue;
}
return new Timestamp()
{
EffectiveDate = signedOnTimestamp,
ExpiryDate = certificateTimestamp,
SignedOn = signedOnTimestamp,
SignatureAlgorithm = GetRegexValue(match, "algorithm")
};
});
}

private string GetRegexValue(Match match, string groupName) =>
match.Success ? match.Groups[groupName].Value : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ public SignatureVerificationManager(Exclusions exclusions, Log log, SignatureVer
AddFileVerifier(new AuthentiCodeVerifier(log, exclusions, options, ".ps1"));
AddFileVerifier(new AuthentiCodeVerifier(log, exclusions, options, ".ps1xml"));
AddFileVerifier(new VsixVerifier(log, exclusions, options));
#else
AddFileVerifier(new PkgVerifier(log, exclusions, options, ".pkg"));
AddFileVerifier(new PkgVerifier(log, exclusions, options, ".app"));
#endif

AddFileVerifier(new LzmaVerifier(log, exclusions, options));
AddFileVerifier(new NupkgVerifier(log, exclusions, options));
AddFileVerifier(new XmlVerifier(log, exclusions, options));
Expand Down

0 comments on commit e266d5f

Please sign in to comment.