Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable unpack/repack of RPM containers #15405

Merged
merged 8 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.DotNet.SignTool" />
<InternalsVisibleTo Include="Microsoft.DotNet.SignTool.Tests" />
</ItemGroup>

<ItemGroup>
<None Include="build/**/*.*"
Pack="true"
Expand Down
21 changes: 19 additions & 2 deletions src/Microsoft.DotNet.Build.Tasks.Installers/src/RpmBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,23 @@ public static string GetRpmHeaderArchitecture(Architecture architecture)
};
}

public static string GetDotNetArchitectureFromRpmHeaderArchitecture(string rpmPackageArchitecture)
{
return rpmPackageArchitecture switch
{
"noarch" => "any",
"i386" => "x86",
"i486" => "x86",
"i586" => "x86",
"i686" => "x86",
"x86_64" => "x64",
"armv6hl" => "arm",
"armv7hl" => "arm",
"aarch64" => "arm64",
_ => rpmPackageArchitecture
};
}

private static short GetRpmOS(OSPlatform os)
{
// See /usr/lib/rpm/rpmrc for the canonical OS mapping
Expand Down Expand Up @@ -204,8 +221,8 @@ public RpmPackage Build()

foreach (var script in _scripts)
{
entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), script.Key), RpmHeaderEntryType.String, "/bin/sh"));
entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), $"{script.Key}prog"), RpmHeaderEntryType.String, script.Value));
entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), script.Key), RpmHeaderEntryType.String, script.Value));
entries.Add(new((RpmHeaderTag)Enum.Parse(typeof(RpmHeaderTag), $"{script.Key}prog"), RpmHeaderEntryType.String, "/bin/sh"));
}

MemoryStream cpioArchive = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public enum RpmHeaderTag
Url = 1020,
OperatingSystem = 1021,
Architecture = 1022,
Prein = 1023,
Postin = 1024,
Preun = 1025,
Postun = 1026,
FileSizes = 1028,
FileModes = 1030,
Expand All @@ -55,7 +57,9 @@ public enum RpmHeaderTag
ChangelogTimestamp = 1080,
ChangelogName = 1081,
ChangelogText = 1082,
Preinprog = 1085,
Postinprog = 1086,
Preunprog = 1087,
Postunprog = 1088,
FileDevices = 1095,
FileInode = 1096,
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.DotNet.SignTool.Tests/FakeSignTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public override bool VerifySignedDeb(TaskLoggingHelper log, string filePath)
return true;
}

public override bool VerifySignedRpm(TaskLoggingHelper log, string filePath)
{
return true;
}

public override bool VerifySignedPowerShellFile(string filePath)
{
return true;
Expand Down
Binary file not shown.
98 changes: 98 additions & 0 deletions src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.Build.Utilities;
using Xunit;
using Xunit.Abstractions;
using Microsoft.DotNet.Build.Tasks.Installers;

namespace Microsoft.DotNet.SignTool.Tests
{
Expand All @@ -34,6 +35,7 @@ public class SignToolTests : IDisposable
{".psc1", new List<SignInfo>{ new SignInfo("PSCCertificate") } },
{".dylib", new List<SignInfo>{ new SignInfo("DylibCertificate") } },
{".deb", new List<SignInfo>{ new SignInfo("LinuxSign") } },
{".rpm", new List<SignInfo>{ new SignInfo("LinuxSign") } },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the RPM extension also need to be added to s_fileExtensionSignInfoWithCollisionId, s_fileExtensionSignInfoPostBuild, and SignableExtensions? It's possible that this may be addressed in #14433

{".dll", new List<SignInfo>{ new SignInfo("Microsoft400") } }, // lgtm [cs/common-default-passwords] Safe, these are certificate names
{".exe", new List<SignInfo>{ new SignInfo("Microsoft400") } }, // lgtm [cs/common-default-passwords] Safe, these are certificate names
{".msi", new List<SignInfo>{ new SignInfo("Microsoft400") } }, // lgtm [cs/common-default-passwords] Safe, these are certificate names
Expand Down Expand Up @@ -447,6 +449,60 @@ private string ExtractArchiveFromDebPackage(string debianPackage, string archive
File.WriteAllBytes(archive, ((MemoryStream)content).ToArray());
return archive;
}

private void ValidateProducedRpmContent(
string rpmPackage,
(string, string)[] expectedFilesOriginalHashes,
string[] signableFiles,
string originalUncompressedPayloadChecksum)
{
string tempDir = Path.Combine(_tmpDir, "verification");
Directory.CreateDirectory(tempDir);

string layout = Path.Combine(tempDir, "layout");
Directory.CreateDirectory(layout);

ZipData.ExtractRpmPayloadContents(rpmPackage, layout);

// Checks:
// Expected files are present
// Signed files have hashes different than original
foreach ((string targetSystemFilePath, string originalHash) in expectedFilesOriginalHashes)
{
string layoutFilePath = Path.Combine(layout, targetSystemFilePath);
File.Exists(layoutFilePath).Should().BeTrue();

using MD5 md5 = MD5.Create(); // lgtm [cs/weak-crypto] Azure Storage specifies use of MD5
using FileStream fileStream = File.OpenRead(layoutFilePath);
string newHash = Convert.ToHexString(md5.ComputeHash(fileStream));

if (signableFiles.Contains(targetSystemFilePath))
{
newHash.Should().NotBe(originalHash);
}
else
{
newHash.Should().Be(originalHash);
}
}

// Checks:
// Header payload digest matches the hash of the payload
// Header payload digest is different than the hash of the original payload
IReadOnlyList<RpmHeader<RpmHeaderTag>.Entry> headerEntries = ZipData.GetRpmHeaderEntries(rpmPackage);
string uncompressedPayloadDigest = ((string[])headerEntries.FirstOrDefault(e => e.Tag == RpmHeaderTag.UncompressedPayloadDigest).Value)[0].ToString();
uncompressedPayloadDigest.Should().NotBe(originalUncompressedPayloadChecksum);

using var stream = File.Open(rpmPackage, FileMode.Open);
using RpmPackage package = RpmPackage.Read(stream);
package.ArchiveStream.Seek(0, SeekOrigin.Begin);
using (SHA256 sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(package.ArchiveStream);
string checksum = Convert.ToHexString(hash).ToLower();
checksum.Should().Be(uncompressedPayloadDigest);
}
}
#endif
[Fact]
public void EmptySigningList()
Expand Down Expand Up @@ -1421,6 +1477,48 @@ public void CheckDebSigning()
ValidateProducedDebContent(Path.Combine(_tmpDir, "test.deb"), expectedFilesOriginalHashes, signableFiles, expectedControlFileContent);
}

[LinuxOnlyFact]
public void CheckRpmSigning()
{
// List of files to be considered for signing
var itemsToSign = new ITaskItem[]
{
new TaskItem(GetResourcePath("test.rpm"))
};

// Default signing information
var strongNameSignInfo = new Dictionary<string, List<SignInfo>>();

// Overriding information
var fileSignInfo = new Dictionary<ExplicitCertificateKey, string>();

ValidateFileSignInfos(itemsToSign, strongNameSignInfo, fileSignInfo, s_fileExtensionSignInfo, new[]
{
"File 'mscorlib.dll' TargetFramework='.NETCoreApp,Version=v10.0' Certificate='Microsoft400'",
"File 'test.rpm' Certificate='LinuxSign'"
});

ValidateGeneratedProject(itemsToSign, strongNameSignInfo, fileSignInfo, s_fileExtensionSignInfo, new[]
{
$@"<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "ContainerSigning", "1", "./usr/local/bin/mscorlib.dll"))}"">
<Authenticode>Microsoft400</Authenticode>
</FilesToSign>",
$@"<FilesToSign Include=""{Uri.EscapeDataString(Path.Combine(_tmpDir, "test.rpm"))}"">
<Authenticode>LinuxSign</Authenticode>
</FilesToSign>"
});

var expectedFilesOriginalHashes = new (string, string)[]
{
("usr/local/bin/hello", "644981BBD6F4ED1B3CF68CD0F47981AA"),
("usr/local/bin/mscorlib.dll", "B80EEBA2B8616B7C37E49B004D69BBB7")
};
string[] signableFiles = ["usr/local/bin/mscorlib.dll"];
string originalUncompressedPayloadChecksum = "216c2a99006d2e14d28a40c0f14a63f6462f533e89789a6f294186e0a0aad3fd";

ValidateProducedRpmContent(Path.Combine(_tmpDir, "test.rpm"), expectedFilesOriginalHashes, signableFiles, originalUncompressedPayloadChecksum);
}

[Fact]
public void VerifyDebIntegrity()
{
Expand Down
30 changes: 30 additions & 0 deletions src/Microsoft.DotNet.SignTool/src/BatchSignUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,17 @@ private void VerifyCertificates(TaskLoggingHelper log)
log.LogError($"Deb package {fileName} must be signed with a LinuxSign certificate.");
}
}
else if (fileName.IsRpm())
{
if (isInvalidEmptyCertificate)
{
log.LogError($"Rpm package {fileName} should have a certificate name.");
}
if (!IsLinuxSignCertificate(fileName.SignInfo.Certificate))
{
log.LogError($"Rpm package {fileName} must be signed with a LinuxSign certificate.");
}
}
else if (fileName.IsNupkg())
{
if(isInvalidEmptyCertificate)
Expand Down Expand Up @@ -577,8 +588,27 @@ private void VerifyAfterSign(TaskLoggingHelper log, FileSignInfo file)
{
_log.LogError($"Deb package {file.FullPath} is not signed properly.");
}
else
{
_log.LogMessage(MessageImportance.Low, $"Deb package {file.FullPath} is signed properly");
}
#endif
}
else if (file.IsRpm())
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
_log.LogMessage(MessageImportance.Low, $"Skipping signature verification of {file.FullPath} on non-Linux platform.");
}
else if (!_signTool.VerifySignedRpm(log, file.FullPath))
{
_log.LogError($"Rpm package {file.FullPath} is not signed properly.");
}
else
{
_log.LogMessage(MessageImportance.Low, $"Rpm package {file.FullPath} is signed properly");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an else here with a "signed properly" message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add it for both DEB and RPM.

}
else if (file.IsPowerShellScript())
{
if (!_signTool.VerifySignedPowerShellFile(file.FullPath))
Expand Down
14 changes: 13 additions & 1 deletion src/Microsoft.DotNet.SignTool/src/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,19 @@ private FileSignInfo ExtractSignInfo(
_log.LogMessage(MessageImportance.Low, $"File {file.FullPath} is signed.");
}
}
else if(FileSignInfo.IsPowerShellScript(file.FullPath))
else if (FileSignInfo.IsRpm(file.FullPath))
{
isAlreadyAuthenticodeSigned = VerifySignatures.VerifySignedRpm(_log, file.FullPath);
if (!isAlreadyAuthenticodeSigned)
{
_log.LogMessage(MessageImportance.Low, $"File {file.FullPath} is not signed.");
}
else
{
_log.LogMessage(MessageImportance.Low, $"File {file.FullPath} is signed.");
}
}
else if (FileSignInfo.IsPowerShellScript(file.FullPath))
{
isAlreadyAuthenticodeSigned = VerifySignatures.VerifySignedPowerShellFile(file.FullPath);
if (!isAlreadyAuthenticodeSigned)
Expand Down
7 changes: 6 additions & 1 deletion src/Microsoft.DotNet.SignTool/src/FileSignInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ internal readonly struct FileSignInfo
internal static bool IsDeb(string path)
=> Path.GetExtension(path) == ".deb";

internal static bool IsRpm(string path)
=> Path.GetExtension(path) == ".rpm";

internal static bool IsPEFile(string path)
=> Path.GetExtension(path) == ".exe" || Path.GetExtension(path) == ".dll";

Expand Down Expand Up @@ -60,10 +63,12 @@ internal static bool IsPackage(string path)
=> IsVsix(path) || IsNupkg(path);

internal static bool IsZipContainer(string path)
=> IsPackage(path) || IsMPack(path) || IsZip(path) || IsTarGZip(path) || IsDeb(path);
=> IsPackage(path) || IsMPack(path) || IsZip(path) || IsTarGZip(path) || IsDeb(path) || IsRpm(path);

internal bool IsDeb() => IsDeb(FileName);

internal bool IsRpm() => IsRpm(FileName);

internal bool IsPEFile() => IsPEFile(FileName);

internal bool IsManaged() => ContentUtil.IsManaged(FullPath);
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.DotNet.SignTool/src/RealSignTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ public override bool VerifySignedDeb(TaskLoggingHelper log, string filePath)
return VerifySignatures.VerifySignedDeb(log, filePath);
}

public override bool VerifySignedRpm(TaskLoggingHelper log, string filePath)
{
return VerifySignatures.VerifySignedRpm(log, filePath);
}

public override bool VerifySignedPowerShellFile(string filePath)
{
return VerifySignatures.VerifySignedPowerShellFile(filePath);
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.DotNet.SignTool/src/SignTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal SignTool(SignToolArgs args, TaskLoggingHelper log)
public abstract bool LocalStrongNameSign(IBuildEngine buildEngine, int round, IEnumerable<FileSignInfo> files);

public abstract bool VerifySignedDeb(TaskLoggingHelper log, string filePath);
public abstract bool VerifySignedRpm(TaskLoggingHelper log, string filePath);
public abstract bool VerifySignedPEFile(Stream stream);
public abstract bool VerifySignedPowerShellFile(string filePath);
public abstract bool VerifySignedNugetFileMarker(string filePath);
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.DotNet.SignTool/src/SignToolConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ internal static class SignToolConstants
".pyd",

".deb",
".rpm",
ellahathaway marked this conversation as resolved.
Show resolved Hide resolved
};

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.DotNet.SignTool/src/ValidationOnlySignTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public override void RemoveStrongNameSign(string assemblyPath)
public override bool VerifySignedDeb(TaskLoggingHelper log, string filePath)
=> true;

public override bool VerifySignedRpm(TaskLoggingHelper log, string filePath)
=> true;

public override bool VerifySignedPEFile(Stream assemblyStream)
=> true;

Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.DotNet.SignTool/src/VerifySignatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ internal static bool VerifySignedDeb(TaskLoggingHelper log, string filePath)
# endif
}

internal static bool VerifySignedRpm(TaskLoggingHelper log, string filePath)
{
// RPM signature verification is not yet implemented
log.LogMessage(MessageImportance.Low, $"Skipping signature verification of {filePath} - not yet implemented.");
return true;
}

internal static bool VerifySignedPowerShellFile(string filePath)
{
return File.ReadLines(filePath).Any(line => line.IndexOf("# SIG # Begin Signature Block", StringComparison.OrdinalIgnoreCase) >= 0);
Expand Down
Loading
Loading