diff --git a/ReleaseBuilder/Build/Command.GpgSign.cs b/ReleaseBuilder/Build/Command.GpgSign.cs new file mode 100644 index 000000000..6f9b2f13e --- /dev/null +++ b/ReleaseBuilder/Build/Command.GpgSign.cs @@ -0,0 +1,52 @@ +namespace ReleaseBuilder.Build; + +public static partial class Command +{ + /// + /// Implementation of the gpg sign command + /// + private static class GpgSign + { + /// + /// Performs a GPG sign operation on the files + /// + /// The files to sign + /// The runtime configuration + /// An awaitable task + public static async Task SignReleaseFiles(IEnumerable files, RuntimeConfig rtcfg) + { + var (gpgid, passphrase) = GetGpgIdAndPassphrase(rtcfg); + + foreach (var file in files) + foreach (var armored in new[] { true, false }) + await ProcessHelper.Execute( + [ + Program.Configuration.Commands.Gpg!, + "--pinentry-mode", "loopback", + "--passphrase-fd", "0", + "--batch", "--yes", + armored ? "--armor" : string.Empty, + "-u", gpgid, + "--output", file + (armored ? "sig.asc" : ".sig"), + "--detach-sign", file + ], + workingDirectory: Path.GetDirectoryName(file), + writeStdIn: (stdin) => stdin.WriteLineAsync(passphrase) + ); + } + + /// + /// Gets the GPG ID and passphrase from the keyfile + /// + /// The runtime configuration + /// The GPG ID and passphrase + static (string GpgId, string GpgPassphrase) GetGpgIdAndPassphrase(RuntimeConfig rtcfg) + { + using var ms = new MemoryStream(); + using var fs = File.OpenRead(Program.Configuration.ConfigFiles.GpgKeyfile); + SharpAESCrypt.SharpAESCrypt.Decrypt(rtcfg.KeyfilePassword, fs, ms); + var parts = System.Text.Encoding.UTF8.GetString(ms.ToArray()).Split('\n', 2, StringSplitOptions.RemoveEmptyEntries); + return (parts[0], parts[1]); + } + } +} diff --git a/ReleaseBuilder/Build/Command.RuntimeConfig.cs b/ReleaseBuilder/Build/Command.RuntimeConfig.cs new file mode 100644 index 000000000..ab6d8b72c --- /dev/null +++ b/ReleaseBuilder/Build/Command.RuntimeConfig.cs @@ -0,0 +1,320 @@ +using System.Security.Cryptography; + +namespace ReleaseBuilder.Build; + +public static partial class Command +{ + /// + /// Setup of the current runtime information + /// + private class RuntimeConfig + { + /// + /// Constructs a new + /// + /// The release info to use + /// The keyfile password to use + /// The sign keys + /// The changelog news + /// The command input + public RuntimeConfig(ReleaseInfo releaseInfo, IEnumerable signKeys, string keyfilePassword, string changelogNews, CommandInput input) + { + ReleaseInfo = releaseInfo; + SignKeys = signKeys; + KeyfilePassword = keyfilePassword; + ChangelogNews = changelogNews; + Input = input; + } + + /// + /// The cached password for the pfx file + /// + private string? _pfxPassword = null; + + /// + /// The commandline input + /// + private CommandInput Input { get; } + + /// + /// The release info for this run + /// + public ReleaseInfo ReleaseInfo { get; } + + /// + /// The keyfile password for this run + /// + public IEnumerable SignKeys { get; } + + /// + /// The primary password + /// + public string KeyfilePassword { get; } + + /// + /// The changelog news + /// + public string ChangelogNews { get; } + + /// + /// Gets the PFX password and throws if not possible + /// + public string PfxPassword + => string.IsNullOrWhiteSpace(_pfxPassword) + ? _pfxPassword = GetAuthenticodePassword(KeyfilePassword) + : _pfxPassword; + + /// + /// Cache value for checking if authenticode signing is enabled + /// + private bool? _useAuthenticodeSigning; + + /// + /// Checks if Authenticode signing should be enabled + /// + public void ToggleAuthenticodeSigning() + { + if (!_useAuthenticodeSigning.HasValue) + { + if (Input.DisableAuthenticode) + { + _useAuthenticodeSigning = false; + return; + } + + if (Program.Configuration.IsAuthenticodePossible()) + _useAuthenticodeSigning = true; + else + { + if (ConsoleHelper.ReadInput("Configuration missing for osslsigncode, continue without signing executables?", "Y", "n") == "Y") + { + _useAuthenticodeSigning = false; + return; + } + + throw new Exception("Configuration is not set up for osslsigncode"); + } + } + } + + /// + /// Cache value for checking if codesign is possible + /// + private bool? _useCodeSignSigning; + + /// + /// Checks if codesign is enabled + /// + public void ToggleSignCodeSigning() + { + if (!_useCodeSignSigning.HasValue) + { + if (Input.DisableSignCode) + { + _useCodeSignSigning = false; + return; + } + + if (!OperatingSystem.IsMacOS()) + _useCodeSignSigning = false; + else if (Program.Configuration.IsCodeSignPossible()) + _useCodeSignSigning = true; + else + { + if (ConsoleHelper.ReadInput("Configuration missing for signcode, continue without signing executables?", "Y", "n") == "Y") + { + _useCodeSignSigning = false; + return; + } + + throw new Exception("Configuration is not set up for signcode"); + } + } + } + + /// + /// Cache value for checking if docker build is enabled + /// + private bool? _dockerBuild; + + /// + /// Checks if docker build is enabled + /// + public async Task ToggleDockerBuild() + { + if (!_dockerBuild.HasValue) + { + try + { + var res = await ProcessHelper.ExecuteWithOutput([Program.Configuration.Commands.Docker!, "ps"], suppressStdErr: true); + _dockerBuild = true; + } + catch + { + + if (ConsoleHelper.ReadInput("Docker does not seem to be running, continue without docker builds?", "Y", "n") == "Y") + { + _dockerBuild = false; + return; + } + + throw new Exception("Docker is not running, and is required for building Docker images"); + } + } + } + + /// + /// Cache value for checking if notarize is enabled + /// + private bool? _useNotarizeSigning; + + /// + /// Checks if notarize signing is enabled + /// + public void ToggleNotarizeSigning() + { + if (!_useNotarizeSigning.HasValue) + { + if (Input.DisableNotarizeSigning) + { + _useNotarizeSigning = false; + return; + } + + if (!OperatingSystem.IsMacOS()) + _useNotarizeSigning = false; + else if (Program.Configuration.IsNotarizePossible()) + _useNotarizeSigning = true; + else + { + if (ConsoleHelper.ReadInput("Configuration missing for notarize, continue without notarizing executables?", "Y", "n") == "Y") + { + _useNotarizeSigning = false; + return; + } + + throw new Exception("Configuration is not set up for notarize"); + } + } + } + + /// + /// Cache value for checking if GPG signing is enabled + /// + private bool? _useGpgSigning; + + public void ToggleGpgSigning() + { + if (!_useGpgSigning.HasValue) + { + if (Input.DisableGpgSigning) + { + _useGpgSigning = false; + return; + } + + if (ConsoleHelper.ReadInput("Configuration missing for gpg, continue without gpg signing packages?", "Y", "n") == "Y") + { + _useGpgSigning = false; + return; + } + + throw new Exception("Configuration is not set up for gpg"); + } + } + + /// + /// Returns a value indicating if codesign is enabled + /// + public bool UseCodeSignSigning => _useCodeSignSigning!.Value; + + /// + /// Returns a value indicating if authenticode signing is enabled + /// + public bool UseAuthenticodeSigning => _useAuthenticodeSigning!.Value; + + /// + /// Returns a value indicating if notarize is enabled + /// + public bool UseNotarizeSigning => _useNotarizeSigning!.Value; + + /// + /// Returns a value indicating if GPG signing is enabled + /// + public bool UseGPGSigning => _useGpgSigning!.Value; + + /// + /// Returns a value indicating if docker build is enabled + /// + public bool UseDockerBuild => _dockerBuild!.Value; + + /// + /// Gets the MacOS app bundle name + /// + public string MacOSAppName => Input.MacOSAppName; + + /// + /// The docker repository to use + /// + public string DockerRepo => Input.DockerRepo; + + /// + /// Gets a value indicating if pushing should be enabled + /// + public bool PushToDocker => !Input.DisableDockerPush; + + /// + /// Decrypts the password file and returns the PFX password + /// + /// Password for the password file + /// The Authenticode password + private string GetAuthenticodePassword(string keyfilepassword) + => EncryptionHelper.DecryptPasswordFile(Program.Configuration.ConfigFiles.AuthenticodePasswordFile, keyfilepassword); + + /// + /// Performs authenticode signing if enabled + /// + /// The file to sign + /// An awaitable task + public Task AuthenticodeSign(string file) + => UseAuthenticodeSigning + ? ProcessRunner.OsslCodeSign( + Program.Configuration.Commands.OsslSignCode!, + Program.Configuration.ConfigFiles.AuthenticodePfxFile, + PfxPassword, + file) + : Task.CompletedTask; + + /// + /// Performs codesign on the given file + /// + /// The file to sign + /// The entitlements to apply + /// An awaitable task + public Task Codesign(string file, string entitlements) + => UseCodeSignSigning + ? ProcessRunner.MacOSCodeSign( + Program.Configuration.Commands.Codesign!, + Program.Configuration.ConfigFiles.CodesignIdentity, + entitlements, + file + ) + : Task.CompletedTask; + + /// + /// Performs productsign on the given file + /// + /// The file to sign + /// An awaitable task + public Task Productsign(string file) + => UseCodeSignSigning + ? ProcessRunner.MacOSProductSign( + Program.Configuration.Commands.Productsign!, + Program.Configuration.ConfigFiles.CodesignIdentity, + file + ) + : Task.CompletedTask; + + } + +} diff --git a/ReleaseBuilder/Build/Command.cs b/ReleaseBuilder/Build/Command.cs index 53350519a..b98b1222a 100644 --- a/ReleaseBuilder/Build/Command.cs +++ b/ReleaseBuilder/Build/Command.cs @@ -63,290 +63,6 @@ public static partial class Command /// private static readonly IReadOnlyList FedoraCLIDepends = []; - /// - /// Setup of the current runtime information - /// - /// The release info for the current build - /// The keyfile password - private class RuntimeConfig - { - /// - /// Constructs a new - /// - /// The release info to use - /// The keyfile password to use - /// The sign keys - /// The changelog news - /// The command input - public RuntimeConfig(ReleaseInfo releaseInfo, IEnumerable signKeys, string keyfilePassword, string changelogNews, CommandInput input) - { - ReleaseInfo = releaseInfo; - SignKeys = signKeys; - KeyfilePassword = keyfilePassword; - ChangelogNews = changelogNews; - Input = input; - } - - /// - /// The cached password for the pfx file - /// - private string? _pfxPassword = null; - - /// - /// The commandline input - /// - private CommandInput Input { get; } - - /// - /// The release info for this run - /// - public ReleaseInfo ReleaseInfo { get; } - - /// - /// The keyfile password for this run - /// - public IEnumerable SignKeys { get; } - - /// - /// The primary password - /// - public string KeyfilePassword { get; } - - /// - /// The changelog news - /// - public string ChangelogNews { get; } - - /// - /// Gets the PFX password and throws if not possible - /// - public string PfxPassword - => string.IsNullOrWhiteSpace(_pfxPassword) - ? _pfxPassword = GetAuthenticodePassword(KeyfilePassword) - : _pfxPassword; - - /// - /// Cache value for checking if authenticode signing is enabled - /// - private bool? _useAuthenticodeSigning; - - /// - /// Checks if Authenticode signing should be enabled - /// - public void ToggleAuthenticodeSigning() - { - if (!_useAuthenticodeSigning.HasValue) - { - if (Input.DisableAuthenticode) - { - _useAuthenticodeSigning = false; - return; - } - - if (Program.Configuration.IsAuthenticodePossible()) - _useAuthenticodeSigning = true; - else - { - if (ConsoleHelper.ReadInput("Configuration missing for osslsigncode, continue without signing executables?", "Y", "n") == "Y") - { - _useAuthenticodeSigning = false; - return; - } - - throw new Exception("Configuration is not set up for osslsigncode"); - } - } - } - - /// - /// Cache value for checking if codesign is possible - /// - private bool? _useCodeSignSigning; - - /// - /// Checks if codesign is enabled - /// - public void ToggleSignCodeSigning() - { - if (!_useCodeSignSigning.HasValue) - { - if (Input.DisableSignCode) - { - _useCodeSignSigning = false; - return; - } - - if (!OperatingSystem.IsMacOS()) - _useCodeSignSigning = false; - else if (Program.Configuration.IsCodeSignPossible()) - _useCodeSignSigning = true; - else - { - if (ConsoleHelper.ReadInput("Configuration missing for signcode, continue without signing executables?", "Y", "n") == "Y") - { - _useCodeSignSigning = false; - return; - } - - throw new Exception("Configuration is not set up for signcode"); - } - } - } - - /// - /// Cache value for checking if docker build is enabled - /// - private bool? _dockerBuild; - - /// - /// Checks if docker build is enabled - /// - public async Task ToggleDockerBuild() - { - if (!_dockerBuild.HasValue) - { - try - { - var res = await ProcessHelper.ExecuteWithOutput([Program.Configuration.Commands.Docker!, "ps"], suppressStdErr: true); - _dockerBuild = true; - } - catch - { - - if (ConsoleHelper.ReadInput("Docker does not seem to be running, continue without docker builds?", "Y", "n") == "Y") - { - _dockerBuild = false; - return; - } - - throw new Exception("Docker is not running, and is required for building Docker images"); - } - } - } - - /// - /// Cache value for checking if notarize is enabled - /// - private bool? _useNotarizeSigning; - - /// - /// Checks if notarize signing is enabled - /// - public void ToggleNotarizeSigning() - { - if (!_useNotarizeSigning.HasValue) - { - if (Input.DisableNotarizeSigning) - { - _useNotarizeSigning = false; - return; - } - - if (!OperatingSystem.IsMacOS()) - _useNotarizeSigning = false; - else if (Program.Configuration.IsNotarizePossible()) - _useNotarizeSigning = true; - else - { - if (ConsoleHelper.ReadInput("Configuration missing for notarize, continue without notarizing executables?", "Y", "n") == "Y") - { - _useNotarizeSigning = false; - return; - } - - throw new Exception("Configuration is not set up for notarize"); - } - } - } - - /// - /// Returns a value indicating if codesign is enabled - /// - public bool UseCodeSignSigning => _useCodeSignSigning!.Value; - - /// - /// Returns a value indicating if authenticode signing is enabled - /// - public bool UseAuthenticodeSigning => _useAuthenticodeSigning!.Value; - - /// - /// Returns a value indicating if notarize is enabled - /// - public bool UseNotarizeSigning => _useNotarizeSigning!.Value; - - /// - /// Returns a value indicating if docker build is enabled - /// - public bool UseDockerBuild => _dockerBuild!.Value; - - /// - /// Gets the MacOS app bundle name - /// - public string MacOSAppName => Input.MacOSAppName; - - /// - /// The docker repository to use - /// - public string DockerRepo => Input.DockerRepo; - - /// - /// Gets a value indicating if pushing should be enabled - /// - public bool PushToDocker => !Input.DisableDockerPush; - - /// - /// Decrypts the password file and returns the PFX password - /// - /// Password for the password file - /// The Authenticode password - private string GetAuthenticodePassword(string keyfilepassword) - => EncryptionHelper.DecryptPasswordFile(Program.Configuration.ConfigFiles.AuthenticodePasswordFile, keyfilepassword); - - /// - /// Performs authenticode signing if enabled - /// - /// The file to sign - /// An awaitable task - public Task AuthenticodeSign(string file) - => UseAuthenticodeSigning - ? ProcessRunner.OsslCodeSign( - Program.Configuration.Commands.OsslSignCode!, - Program.Configuration.ConfigFiles.AuthenticodePfxFile, - PfxPassword, - file) - : Task.CompletedTask; - - /// - /// Performs codesign on the given file - /// - /// The file to sign - /// The entitlements to apply - /// An awaitable task - public Task Codesign(string file, string entitlements) - => UseCodeSignSigning - ? ProcessRunner.MacOSCodeSign( - Program.Configuration.Commands.Codesign!, - Program.Configuration.ConfigFiles.CodesignIdentity, - entitlements, - file - ) - : Task.CompletedTask; - - /// - /// Performs productsign on the given file - /// - /// The file to sign - /// An awaitable task - public Task Productsign(string file) - => UseCodeSignSigning - ? ProcessRunner.MacOSProductSign( - Program.Configuration.Commands.Productsign!, - Program.Configuration.ConfigFiles.CodesignIdentity, - file - ) - : Task.CompletedTask; - - } /// /// Structure for keeping all variables for a single release @@ -468,6 +184,12 @@ public static partial class Command getDefaultValue: () => false ); + var disableGpgSigningOption = new Option( + name: "--disable-gpg-signing", + description: "Disables GPG signing of packages", + getDefaultValue: () => false + ); + var command = new System.CommandLine.Command("build", "Builds the packages for a release") { gitStashPushOption, releaseChannelOption, @@ -482,7 +204,8 @@ public static partial class Command disableDockerPushOption, dockerRepoOption, changelogFileOption, - disableNotarizeSigningOption + disableNotarizeSigningOption, + disableGpgSigningOption }; command.Handler = CommandHandler.Create(DoBuild); @@ -506,6 +229,7 @@ public static partial class Command /// The docker repository to push to /// The path to the changelog file /// If notarize signing should be disabled + /// If GPG signing should be disabled record CommandInput( PackageTarget[] Targets, DirectoryInfo BuildPath, @@ -520,7 +244,8 @@ public static partial class Command string MacOSAppName, string DockerRepo, FileInfo ChangelogFile, - bool DisableNotarizeSigning + bool DisableNotarizeSigning, + bool DisableGpgSigning ); static async Task DoBuild(CommandInput input) @@ -612,6 +337,7 @@ public static partial class Command rtcfg.ToggleAuthenticodeSigning(); rtcfg.ToggleSignCodeSigning(); rtcfg.ToggleNotarizeSigning(); + rtcfg.ToggleGpgSigning(); await rtcfg.ToggleDockerBuild(); if (!rtcfg.UseDockerBuild) @@ -688,10 +414,23 @@ public static partial class Command }) ); - File.Move(Path.Combine(input.BuildPath.FullName, "packages", "autoupdate.manifest"), Path.Combine(input.BuildPath.FullName, "packages", "latest-v2.manifest"), true); + File.Move( + Path.Combine(input.BuildPath.FullName, "packages", "autoupdate.manifest"), + Path.Combine(input.BuildPath.FullName, "packages", "latest-v2.manifest"), + true + ); + + var files = builtPackages.Select(x => x.CreatedFile).Append("latest-v2.manifest").ToList(); + + // Create the GPG signatures for the files + if (rtcfg.UseGPGSigning) + { + Console.WriteLine("Creating GPG signatures ..."); + await GpgSign.SignReleaseFiles(files, rtcfg); + } Console.WriteLine("Build completed, uploading packages ..."); - var files = builtPackages.Select(x => x.CreatedFile).Append("latest-v2.manifest").ToArray(); + Console.WriteLine("Upload completed, releasing packages ...");