Skip to content
Open
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
11 changes: 11 additions & 0 deletions docs/gh-build-and-sign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ jobs:
--azure-credential-type "azure-cli"
--azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}" # This does not need to be a secret and is just a placeholder
--azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" # This does not need to be a secret and is just a placeholder
--certificate-output "${{ runner.temp }}\signing-certificate.cer"

# Make the public certificate path available to later steps in this job.
- name: Publish signing certificate path
id: signing-certificate
shell: pwsh
run: '"certificate_file=${{ runner.temp }}\signing-certificate.cer" >> $env:GITHUB_OUTPUT'

- name: Use signing certificate
shell: pwsh
run: Get-PfxCertificate -FilePath "${{ steps.signing-certificate.outputs.certificate_file }}"

# Publish the signed packages
- name: Upload build artifacts
Expand Down
2 changes: 1 addition & 1 deletion src/Sign.Cli/ArtifactSigningCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ internal ArtifactSigningCommand(CodeCommand codeCommand, IServiceProviderFactory

ArtifactSigningServiceProvider trustedSigningServiceProvider = new();

return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument);
return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument, cancellationToken);
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Sign.Cli/AzureKeyVaultCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ internal AzureKeyVaultCommand(CodeCommand codeCommand, IServiceProviderFactory s

KeyVaultServiceProvider keyVaultServiceProvider = new();

return codeCommand.HandleAsync(parseResult, serviceProviderFactory, keyVaultServiceProvider, filesArgument);
return codeCommand.HandleAsync(parseResult, serviceProviderFactory, keyVaultServiceProvider, filesArgument, cancellationToken);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/Sign.Cli/CertificateStoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ internal CertificateStoreCommand(CodeCommand codeCommand, IServiceProviderFactor
useMachineKeyContainer,
isInteractive);

return codeCommand.HandleAsync(parseResult, serviceProviderFactory, certificateStoreServiceProvider, filesArgument);
return codeCommand.HandleAsync(parseResult, serviceProviderFactory, certificateStoreServiceProvider, filesArgument, cancellationToken);
});
}

Expand Down
42 changes: 41 additions & 1 deletion src/Sign.Cli/CodeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.CommandLine.Parsing;
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
Expand All @@ -19,6 +20,7 @@ internal sealed class CodeCommand : Command
{
internal Option<string?> ApplicationNameOption { get; }
internal Option<DirectoryInfo> BaseDirectoryOption { get; }
internal Option<string?> CertificateOutputOption { get; }
internal Option<string> DescriptionOption { get; }
internal Option<Uri?> DescriptionUrlOption { get; }
internal Option<HashAlgorithmName> FileDigestOption { get; }
Expand Down Expand Up @@ -46,6 +48,11 @@ internal CodeCommand()
Description = Resources.BaseDirectoryOptionDescription,
Recursive = true
};
CertificateOutputOption = new Option<string?>("--certificate-output", "-co")
{
Description = Resources.CertificateOutputOptionDescription,
Recursive = true
};
DescriptionOption = new Option<string>("--description", "-d")
{
Description = Resources.DescriptionOptionDescription,
Expand Down Expand Up @@ -120,6 +127,7 @@ internal CodeCommand()
Options.Add(DescriptionUrlOption);
Options.Add(BaseDirectoryOption);
Options.Add(OutputOption);
Options.Add(CertificateOutputOption);
Options.Add(PublisherNameOption);
Options.Add(FileListOption);
Options.Add(RecurseContainersOption);
Expand All @@ -130,7 +138,12 @@ internal CodeCommand()
Options.Add(VerbosityOption);
}

internal async Task<int> HandleAsync(ParseResult parseResult, IServiceProviderFactory serviceProviderFactory, ISignatureProvider signatureProvider, IEnumerable<string> filesArgument)
internal async Task<int> HandleAsync(
ParseResult parseResult,
IServiceProviderFactory serviceProviderFactory,
ISignatureProvider signatureProvider,
IEnumerable<string> filesArgument,
CancellationToken cancellationToken)
{
// Some of the options have a default value and that is why we can safely use
// the null-forgiving operator (!) to simplify the code.
Expand All @@ -146,6 +159,7 @@ internal async Task<int> HandleAsync(ParseResult parseResult, IServiceProviderFa
Uri timestampUrl = parseResult.GetValue(TimestampUrlOption)!;
LogLevel verbosity = parseResult.GetValue(VerbosityOption);
string? output = parseResult.GetValue(OutputOption);
string? certificateOutput = parseResult.GetValue(CertificateOutputOption);
int maxConcurrency = parseResult.GetValue(MaxConcurrencyOption);

// Make sure this is rooted
Expand Down Expand Up @@ -246,6 +260,8 @@ internal async Task<int> HandleAsync(ParseResult parseResult, IServiceProviderFa

ISigner signer = serviceProvider.GetRequiredService<ISigner>();

cancellationToken.ThrowIfCancellationRequested();

int exitCode = await signer.SignAsync(
inputFiles,
output,
Expand All @@ -261,9 +277,33 @@ internal async Task<int> HandleAsync(ParseResult parseResult, IServiceProviderFa
fileHashAlgorithmName,
timestampHashAlgorithmName);

if (exitCode == ExitCode.Success && !string.IsNullOrEmpty(certificateOutput))
{
FileInfo certificateOutputFile = new(ExpandFilePath(baseDirectory, certificateOutput));
ICertificateProvider certificateProvider = serviceProvider.GetRequiredService<ICertificateProvider>();

await ExportCertificateAsync(certificateProvider, certificateOutputFile, cancellationToken);
}

return exitCode;
}

internal static async Task ExportCertificateAsync(
ICertificateProvider certificateProvider,
FileInfo certificateOutputFile,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(certificateProvider, nameof(certificateProvider));
ArgumentNullException.ThrowIfNull(certificateOutputFile, nameof(certificateOutputFile));

certificateOutputFile.Directory!.Create();

using X509Certificate2 certificate = await certificateProvider.GetCertificateAsync(cancellationToken);
byte[] certificateBytes = certificate.Export(X509ContentType.Cert);

await File.WriteAllBytesAsync(certificateOutputFile.FullName, certificateBytes, cancellationToken);
}

private static string ExpandFilePath(DirectoryInfo baseDirectory, string file)
{
if (Path.IsPathRooted(file))
Expand Down
9 changes: 9 additions & 0 deletions src/Sign.Cli/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Sign.Cli/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@
<data name="BaseDirectoryOptionDescription" xml:space="preserve">
<value>Base directory for files. Overrides the current working directory.</value>
</data>
<data name="CertificateOutputOptionDescription" xml:space="preserve">
<value>Write the public signing certificate as a DER-encoded CER file.</value>
</data>
<data name="CertificateStoreCommandDescription" xml:space="preserve">
<value>Use Windows Certificate Store or a local certificate file.</value>
</data>
Expand Down
2 changes: 1 addition & 1 deletion src/Sign.Cli/TrustedSigningCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ internal TrustedSigningCommand(CodeCommand codeCommand, IServiceProviderFactory

ArtifactSigningServiceProvider trustedSigningServiceProvider = new();

return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument);
return codeCommand.HandleAsync(parseResult, serviceProviderFactory, trustedSigningServiceProvider, filesArgument, cancellationToken);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I personally think it would make more sense to make it a specific option for this command and execute it after this handle command?

@Jaxelr Jaxelr Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1 to this for the ergonomy and usability. Its more straightforward to have a command to retrieve the pubkey, although to be fair there's a small tiny percentage of possibility that the certificate might've rotated when the get is executed after the sign, this way we guarantee we use the same certificate on the operation.

});
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Základní adresář pro soubory Přepíše aktuální pracovní adresář.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Použijte úložiště certifikátů systému Windows nebo místní soubor certifikátu.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Basisverzeichnis für Dateien. Überschreibt das aktuelle Arbeitsverzeichnis.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Verwenden Sie den Windows-Zertifikatspeicher oder eine lokale Zertifikatdatei.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Directorio base para los archivos. Invalida el directorio de trabajo actual.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Use el Almacén de certificados de Windows o un archivo de certificados local.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Répertoire de base pour les fichiers. Remplace le répertoire de travail actuel.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Utilisez le magasin de certificats Windows ou un fichier local du certificat.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.it.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Directory di base per i file. Esegue l'override della directory di lavoro corrente.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Utilizzare l'archivio certificati di Windows o un file di certificato locale.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.ja.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">ファイルのベース ディレクトリ。 現在の作業ディレクトリをオーバーライドします。</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Windows 証明書ストアまたはローカル証明書ファイルを使用します。</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.ko.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">파일의 기본 디렉터리입니다. 현재 작업 디렉터리를 재정의합니다.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Windows 인증서 저장소 또는 로컬 인증서 파일을 사용합니다.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.pl.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Katalog podstawowy dla plików. Zastępuje bieżący katalog roboczy.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Użyj magazynu certyfikatów systemu Windows lub lokalnego pliku certyfikatów.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.pt-BR.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Diretório base para arquivos. Substitui o diretório de trabalho atual.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Use o Repositório de Certificados do Windows ou um arquivo de certificado local.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.ru.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Базовый каталог для файлов. Переопределяет текущий рабочий каталог.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Используйте хранилище сертификатов Windows или локальный файл сертификата.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.tr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Dosyalar için temel dizin. Geçerli çalışma dizinini geçersiz kılar.</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">Windows Sertifika Deposu veya yerel bir sertifika dosyası kullanın.</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.zh-Hans.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">文件的基目录。替代当前工作目录。</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">使用 Windows 证书存储或本地证书文件。</target>
Expand Down
5 changes: 5 additions & 0 deletions src/Sign.Cli/xlf/Resources.zh-Hant.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<target state="translated">檔案的基底目錄。覆寫目前的工作目錄。</target>
<note />
</trans-unit>
<trans-unit id="CertificateOutputOptionDescription">
<source>Write the public signing certificate as a DER-encoded CER file.</source>
<target state="new">Write the public signing certificate as a DER-encoded CER file.</target>
<note />
</trans-unit>
<trans-unit id="CertificateStoreCommandDescription">
<source>Use Windows Certificate Store or a local certificate file.</source>
<target state="translated">使用 Windows 憑證存放區或本機憑證檔案。</target>
Expand Down
Loading