diff --git a/src/NuGet.Clients/NuGet.CommandLine/Commands/SignCommand.cs b/src/NuGet.Clients/NuGet.CommandLine/Commands/SignCommand.cs index fa0a6e8c715..3d1766424fe 100644 --- a/src/NuGet.Clients/NuGet.CommandLine/Commands/SignCommand.cs +++ b/src/NuGet.Clients/NuGet.CommandLine/Commands/SignCommand.cs @@ -111,7 +111,8 @@ public SignArgs GetSignArgs() Overwrite = Overwrite, NonInteractive = NonInteractive, Timestamper = Timestamper, - TimestampHashAlgorithm = timestampHashAlgorithm + TimestampHashAlgorithm = timestampHashAlgorithm, + PasswordProvider = new ConsolePasswordProvider(Console) }; } diff --git a/src/NuGet.Clients/NuGet.CommandLine/ConsolePasswordProvider.cs b/src/NuGet.Clients/NuGet.CommandLine/ConsolePasswordProvider.cs new file mode 100644 index 00000000000..f2a8ed3d4f1 --- /dev/null +++ b/src/NuGet.Clients/NuGet.CommandLine/ConsolePasswordProvider.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Commands.SignCommand; + +namespace NuGet.CommandLine +{ + /// + /// Allows requesting a user to input their password through Console. + /// + internal class ConsolePasswordProvider : IPasswordProvider + { + private IConsole _console; + + public ConsolePasswordProvider(IConsole console) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + +#if IS_DESKTOP + /// + /// Requests user to input password and returns it as a SecureString on Console. + /// + /// Path to the file that needs a password to open. + /// Cancellation token. + /// SecureString containing the user input password. The SecureString should be disposed after use. + public Task GetPassword(string filePath, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + var password = new SecureString(); + + _console.WriteLine(string.Format(CultureInfo.CurrentCulture, NuGetResources.ConsolePasswordProvider_DisplayFile, filePath)); + _console.Write(NuGetResources.ConsolePasswordProvider_PromptForPassword); + _console.ReadSecureString(password); + + return Task.FromResult(password); + } +#endif + } +} diff --git a/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.Designer.cs b/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.Designer.cs index 54943d70660..14519275d43 100644 --- a/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.Designer.cs +++ b/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.Designer.cs @@ -19,7 +19,7 @@ namespace NuGet.CommandLine { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class NuGetResources { @@ -1095,6 +1095,24 @@ public static string ConsoleConfirmMessageAccept_trk { } } + /// + /// Looks up a localized string similar to Please provide password for: {0}. + /// + public static string ConsolePasswordProvider_DisplayFile { + get { + return ResourceManager.GetString("ConsolePasswordProvider_DisplayFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password: . + /// + public static string ConsolePasswordProvider_PromptForPassword { + get { + return ResourceManager.GetString("ConsolePasswordProvider_PromptForPassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to The remote server indicated that the previous request was forbidden. Please provide credentials for: {0}. /// diff --git a/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.resx b/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.resx index 4454c4d21f3..09d97004e21 100644 --- a/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.resx +++ b/src/NuGet.Clients/NuGet.CommandLine/NuGetResources.resx @@ -6151,4 +6151,11 @@ Oluşturma sırasında NuGet'in paketleri indirmesini önlemek için, Visual Stu Response file '{0}' cannot be larger than {1}mb + + Please provide password for: {0} + 0 - file that requires password + + + Password: + \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateFindOptions.cs b/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateFindOptions.cs index 8f67b62b37a..8a44b674d43 100644 --- a/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateFindOptions.cs +++ b/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateFindOptions.cs @@ -4,6 +4,9 @@ using System; using System.Security; using System.Security.Cryptography.X509Certificates; +using System.Threading; +using NuGet.Commands.SignCommand; +using NuGet.Common; namespace NuGet.Commands { @@ -43,5 +46,20 @@ internal class CertificateSourceOptions /// public string Fingerprint { get; set; } + /// + /// bool used to indicate if the user can be prompted for password. + /// + public bool NonInteractive { get; set; } + + /// + /// Password provider to get the password from user for opening a pfx file. + /// + public IPasswordProvider PasswordProvider { get; set; } + + /// + /// Cancellation token. + /// + public CancellationToken Token { get; set; } + } } diff --git a/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateProvider.cs b/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateProvider.cs index 86980ef7aee..213792115e2 100644 --- a/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateProvider.cs +++ b/src/NuGet.Core/NuGet.Commands/SignCommand/CertificateProvider.cs @@ -3,8 +3,12 @@ using System; using System.Globalization; +using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using NuGet.Common; namespace NuGet.Commands { @@ -32,7 +36,7 @@ internal static class CertificateProvider /// CertificateSourceOptions to be used while searching for the certificates. /// An X509Certificate2Collection object containing matching certificates. /// If no matching certificates are found then it returns an empty collection. - public static X509Certificate2Collection GetCertificates(CertificateSourceOptions options) + public static async Task GetCertificatesAsync(CertificateSourceOptions options) { // check certificate path var resultCollection = new X509Certificate2Collection(); @@ -40,16 +44,7 @@ public static X509Certificate2Collection GetCertificates(CertificateSourceOption { try { - X509Certificate2 cert; - - if (!string.IsNullOrEmpty(options.CertificatePassword)) - { - cert = new X509Certificate2(options.CertificatePath, options.CertificatePassword); // use the password if the user provided it. - } - else - { - cert = new X509Certificate2(options.CertificatePath); - } + var cert = await LoadCertificateFromFileAsync(options); resultCollection = new X509Certificate2Collection(cert); } @@ -82,25 +77,70 @@ public static X509Certificate2Collection GetCertificates(CertificateSourceOption } else { - var store = new X509Store(options.StoreName, options.StoreLocation); + resultCollection = LoadCertificateFromStore(options); + } + + return resultCollection; + } - OpenStore(store); + private static async Task LoadCertificateFromFileAsync(CertificateSourceOptions options) + { + X509Certificate2 cert; - if (!string.IsNullOrEmpty(options.Fingerprint)) + if (!string.IsNullOrEmpty(options.CertificatePassword)) + { + cert = new X509Certificate2(options.CertificatePath, options.CertificatePassword); // use the password if the user provided it. + } + else + { +#if IS_DESKTOP + try { - resultCollection = store.Certificates.Find(X509FindType.FindByThumbprint, options.Fingerprint, validOnly: false); + cert = new X509Certificate2(options.CertificatePath); } - - if (!string.IsNullOrEmpty(options.SubjectName)) + catch (CryptographicException ex) { - resultCollection = store.Certificates.Find(X509FindType.FindBySubjectName, options.SubjectName, validOnly: false); + // prompt user for password if needed + if (ex.HResult == ERROR_INVALID_PASSWORD_HRESULT && + !options.NonInteractive) + { + using (var password = await options.PasswordProvider.GetPassword(options.CertificatePath, options.Token)) + { + cert = new X509Certificate2(options.CertificatePath, password); + } + } + else + { + throw ex; + } } - -#if IS_DESKTOP - store.Close(); +#else + cert = new X509Certificate2(options.CertificatePath); #endif } + return cert; + } + + private static X509Certificate2Collection LoadCertificateFromStore(CertificateSourceOptions options) + { + var resultCollection = new X509Certificate2Collection(); + var store = new X509Store(options.StoreName, options.StoreLocation); + + OpenStore(store); + + if (!string.IsNullOrEmpty(options.Fingerprint)) + { + resultCollection = store.Certificates.Find(X509FindType.FindByThumbprint, options.Fingerprint, validOnly: true); + } + else if (!string.IsNullOrEmpty(options.SubjectName)) + { + resultCollection = store.Certificates.Find(X509FindType.FindBySubjectName, options.SubjectName, validOnly: true); + } + +#if IS_DESKTOP + store.Close(); +#endif return resultCollection; } diff --git a/src/NuGet.Core/NuGet.Commands/SignCommand/IPasswordProvider.cs b/src/NuGet.Core/NuGet.Commands/SignCommand/IPasswordProvider.cs new file mode 100644 index 00000000000..fc5b1094d62 --- /dev/null +++ b/src/NuGet.Core/NuGet.Commands/SignCommand/IPasswordProvider.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security; +using System.Threading; +using System.Threading.Tasks; + +namespace NuGet.Commands.SignCommand +{ + public interface IPasswordProvider + { +// Currently there is no cross platform interactive scenario +#if IS_DESKTOP + /// + /// Requests user to input password and returns it as a SecureString. + /// + /// Path to the file that needs a password to open. + /// Cancellation token. + /// SecureString containing the user input password. The SecureString should be disposed after use. + Task GetPassword(string filePath, CancellationToken token); +#endif + } +} diff --git a/src/NuGet.Core/NuGet.Commands/SignCommand/SignArgs.cs b/src/NuGet.Core/NuGet.Commands/SignCommand/SignArgs.cs index 33bbe85d95d..0a271b850be 100644 --- a/src/NuGet.Core/NuGet.Commands/SignCommand/SignArgs.cs +++ b/src/NuGet.Core/NuGet.Commands/SignCommand/SignArgs.cs @@ -4,8 +4,8 @@ using System; using System.Security.Cryptography.X509Certificates; using System.Threading; +using NuGet.Commands.SignCommand; using NuGet.Common; -using NuGet.Packaging.Signing; namespace NuGet.Commands { @@ -94,6 +94,11 @@ public class SignArgs /// public ILogger Logger { get; set; } + /// + /// Password provider to get the password from user for opening a pfx file. + /// + public IPasswordProvider PasswordProvider { get; set; } + /// /// Cancellation Token. /// diff --git a/src/NuGet.Core/NuGet.Commands/SignCommand/SignCommandRunner.cs b/src/NuGet.Core/NuGet.Commands/SignCommand/SignCommandRunner.cs index c8f5b99929a..b272d6f87a8 100644 --- a/src/NuGet.Core/NuGet.Commands/SignCommand/SignCommandRunner.cs +++ b/src/NuGet.Core/NuGet.Commands/SignCommand/SignCommandRunner.cs @@ -29,7 +29,7 @@ public async Task ExecuteCommandAsync(SignArgs signArgs) var packagesToSign = LocalFolderUtility.ResolvePackageFromPath(signArgs.PackagePath); LocalFolderUtility.EnsurePackageFileExists(signArgs.PackagePath, packagesToSign); - var cert = GetCertificate(signArgs); + var cert = await GetCertificateAsync(signArgs); signArgs.Logger.LogInformation(Environment.NewLine); signArgs.Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, @@ -174,7 +174,7 @@ private SignPackageRequest GenerateSignPackageRequest(SignArgs signArgs, X509Cer }; } - private static X509Certificate2 GetCertificate(SignArgs signArgs) + private static async Task GetCertificateAsync(SignArgs signArgs) { var certFindOptions = new CertificateSourceOptions() { @@ -183,11 +183,14 @@ private static X509Certificate2 GetCertificate(SignArgs signArgs) Fingerprint = signArgs.CertificateFingerprint, StoreLocation = signArgs.CertificateStoreLocation, StoreName = signArgs.CertificateStoreName, - SubjectName = signArgs.CertificateSubjectName + SubjectName = signArgs.CertificateSubjectName, + NonInteractive = signArgs.NonInteractive, + PasswordProvider = signArgs.PasswordProvider, + Token = signArgs.Token }; // get matching certificates - var matchingCertCollection = CertificateProvider.GetCertificates(certFindOptions); + var matchingCertCollection = await CertificateProvider.GetCertificatesAsync(certFindOptions); if (matchingCertCollection.Count > 1) { diff --git a/test/NuGet.Clients.FuncTests/NuGet.CommandLine.FuncTest/Commands/SignCommandTests.cs b/test/NuGet.Clients.FuncTests/NuGet.CommandLine.FuncTest/Commands/SignCommandTests.cs index c42a7d22132..affb6d2f52b 100644 --- a/test/NuGet.Clients.FuncTests/NuGet.CommandLine.FuncTest/Commands/SignCommandTests.cs +++ b/test/NuGet.Clients.FuncTests/NuGet.CommandLine.FuncTest/Commands/SignCommandTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using FluentAssertions; using NuGet.Test.Utility; @@ -17,6 +18,8 @@ namespace NuGet.CommandLine.FuncTest.Commands public class SignCommandTests { private const string _packageAlreadySignedError = "Error NU5000: The package already contains a signature. Please remove the existing signature before adding a new signature."; + private const string _invalidPasswordError = @"Invalid password was provided for the certificate file '{0}'. Please provide a valid password using the '-CertificatePassword' option"; + private const string _noTimestamperWarningCode = "NU3521"; private SignCommandTestFixture _testFixture; private TrustedTestCert _trustedTestCert; @@ -40,11 +43,11 @@ public void SignCommand_SignPackage() using (var dir = TestDirectory.Create()) using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) { - var packagePath = Path.Combine(dir, new Guid().ToString()); + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); - using (Stream fileStream = File.OpenWrite(packagePath)) + using (var fileStream = File.OpenWrite(packagePath)) { zipStream.CopyTo(fileStream); } @@ -59,7 +62,7 @@ public void SignCommand_SignPackage() // Assert result.Success.Should().BeTrue(); - result.AllOutput.Should().Contain("NU3521"); + result.AllOutput.Should().Contain(_noTimestamperWarningCode); } } @@ -72,11 +75,11 @@ public void SignCommand_SignPackageWithTimestamping() using (var dir = TestDirectory.Create()) using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) { - var packagePath = Path.Combine(dir, new Guid().ToString()); + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); - using (Stream fileStream = File.OpenWrite(packagePath)) + using (var fileStream = File.OpenWrite(packagePath)) { zipStream.CopyTo(fileStream); } @@ -91,7 +94,7 @@ public void SignCommand_SignPackageWithTimestamping() // Assert result.Success.Should().BeTrue(); - result.AllOutput.Should().NotContain("NU3521"); + result.AllOutput.Should().NotContain(_noTimestamperWarningCode); } } @@ -105,12 +108,13 @@ public void SignCommand_SignPackageWithOutputDirectory() using (var outputDir = TestDirectory.Create()) using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) { - var packagePath = Path.Combine(dir, new Guid().ToString()); - var signedPackagePath = Path.Combine(dir, new Guid().ToString()); + var packageFileName = Guid.NewGuid().ToString(); + var packagePath = Path.Combine(dir, packageFileName); + var signedPackagePath = Path.Combine(outputDir, packageFileName); zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); - using (Stream fileStream = File.OpenWrite(packagePath)) + using (var fileStream = File.OpenWrite(packagePath)) { zipStream.CopyTo(fileStream); } @@ -125,7 +129,7 @@ public void SignCommand_SignPackageWithOutputDirectory() // Assert result.Success.Should().BeTrue(); - result.AllOutput.Should().Contain("NU3521"); + result.AllOutput.Should().Contain(_noTimestamperWarningCode); File.Exists(signedPackagePath).Should().BeTrue(); } } @@ -139,11 +143,11 @@ public void SignCommand_ResignPackageWithoutOverwriteFails() using (var dir = TestDirectory.Create()) using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) { - var packagePath = Path.Combine(dir, new Guid().ToString()); + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); - using (Stream fileStream = File.OpenWrite(packagePath)) + using (var fileStream = File.OpenWrite(packagePath)) { zipStream.CopyTo(fileStream); } @@ -165,14 +169,14 @@ public void SignCommand_ResignPackageWithoutOverwriteFails() // Assert firstResult.Success.Should().BeTrue(); - firstResult.AllOutput.Should().Contain("NU3521"); + firstResult.AllOutput.Should().Contain(_noTimestamperWarningCode); secondResult.Success.Should().BeFalse(); secondResult.Errors.Should().Contain(_packageAlreadySignedError); } } [Fact] - public void SignCommand_ResignPackageWithOverwriteFails() + public void SignCommand_ResignPackageWithOverwriteSuccess() { // Arrange var testLogger = new TestLogger(); @@ -180,11 +184,11 @@ public void SignCommand_ResignPackageWithOverwriteFails() using (var dir = TestDirectory.Create()) using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) { - var packagePath = Path.Combine(dir, new Guid().ToString()); + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); - using (Stream fileStream = File.OpenWrite(packagePath)) + using (var fileStream = File.OpenWrite(packagePath)) { zipStream.CopyTo(fileStream); } @@ -206,9 +210,232 @@ public void SignCommand_ResignPackageWithOverwriteFails() // Assert firstResult.Success.Should().BeTrue(); - firstResult.AllOutput.Should().Contain("NU3521"); + firstResult.AllOutput.Should().Contain(_noTimestamperWarningCode); secondResult.Success.Should().BeTrue(); - secondResult.AllOutput.Should().Contain("NU3521"); + secondResult.AllOutput.Should().Contain(_noTimestamperWarningCode); + } + } + + [Fact] + public void SignCommand_SignPackageWithPfxFileSuccess() + { + // Arrange + var testLogger = new TestLogger(); + + using (var dir = TestDirectory.Create()) + using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) + { + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); + var pfxPath = Path.Combine(dir, Guid.NewGuid().ToString()); + + var password = Guid.NewGuid().ToString(); + var pfxBytes = _trustedTestCert.Source.Cert.Export(X509ContentType.Pfx, password); + + using (var fileStream = File.OpenWrite(pfxPath)) + using (var pfxStream = new MemoryStream(pfxBytes)) + { + pfxStream.CopyTo(fileStream); + } + + zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); + + using (var fileStream = File.OpenWrite(packagePath)) + { + zipStream.CopyTo(fileStream); + } + + // Act + var firstResult = CommandRunner.Run( + _nugetExePath, + dir, + $"sign {packagePath} -CertificatePath {pfxPath} -CertificatePassword {password}", + waitForExit: true, + timeOutInMilliseconds: 10000); + + // Assert + firstResult.Success.Should().BeTrue(); + firstResult.AllOutput.Should().Contain(_noTimestamperWarningCode); + } + } + + + [Fact] + public void SignCommand_SignPackageWithPfxFileInteractiveSuccess() + { + // Arrange + var testLogger = new TestLogger(); + + using (var dir = TestDirectory.Create()) + using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) + { + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); + var pfxPath = Path.Combine(dir, Guid.NewGuid().ToString()); + + var password = Guid.NewGuid().ToString(); + var pfxBytes = _trustedTestCert.Source.Cert.Export(X509ContentType.Pfx, password); + + using (var fileStream = File.OpenWrite(pfxPath)) + using (var pfxStream = new MemoryStream(pfxBytes)) + { + pfxStream.CopyTo(fileStream); + } + + zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); + + using (var fileStream = File.OpenWrite(packagePath)) + { + zipStream.CopyTo(fileStream); + } + + // Act + var firstResult = CommandRunner.Run( + _nugetExePath, + dir, + $"sign {packagePath} -CertificatePath {pfxPath}", + waitForExit: true, + inputAction: (w) => + { + w.WriteLine(password); + }, + timeOutInMilliseconds: 10000); + + // Assert + firstResult.Success.Should().BeTrue(); + firstResult.AllOutput.Should().Contain(_noTimestamperWarningCode); + } + } + + [Fact] + public void SignCommand_SignPackageWithPfxFileInteractiveInvalidPasswordFails() + { + // Arrange + var testLogger = new TestLogger(); + + using (var dir = TestDirectory.Create()) + using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) + { + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); + var pfxPath = Path.Combine(dir, Guid.NewGuid().ToString()); + + var password = Guid.NewGuid().ToString(); + var pfxBytes = _trustedTestCert.Source.Cert.Export(X509ContentType.Pfx, password); + + using (var fileStream = File.OpenWrite(pfxPath)) + using (var pfxStream = new MemoryStream(pfxBytes)) + { + pfxStream.CopyTo(fileStream); + } + + zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); + + using (var fileStream = File.OpenWrite(packagePath)) + { + zipStream.CopyTo(fileStream); + } + + // Act + var firstResult = CommandRunner.Run( + _nugetExePath, + dir, + $"sign {packagePath} -CertificatePath {pfxPath}", + waitForExit: true, + inputAction: (w) => + { + w.WriteLine(Guid.NewGuid().ToString()); + }, + timeOutInMilliseconds: 10000); + + // Assert + firstResult.Success.Should().BeFalse(); + firstResult.AllOutput.Should().Contain(string.Format(_invalidPasswordError, pfxPath)); + } + } + + [Fact] + public void SignCommand_SignPackageWithPfxFileWithoutPasswordAndWithNonInteractiveFails() + { + // Arrange + var testLogger = new TestLogger(); + + using (var dir = TestDirectory.Create()) + using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) + { + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); + var pfxPath = Path.Combine(dir, Guid.NewGuid().ToString()); + + var password = Guid.NewGuid().ToString(); + var pfxBytes = _trustedTestCert.Source.Cert.Export(X509ContentType.Pfx, password); + + using (var fileStream = File.OpenWrite(pfxPath)) + using (var pfxStream = new MemoryStream(pfxBytes)) + { + pfxStream.CopyTo(fileStream); + } + + zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); + + using (var fileStream = File.OpenWrite(packagePath)) + { + zipStream.CopyTo(fileStream); + } + + // Act + var firstResult = CommandRunner.Run( + _nugetExePath, + dir, + $"sign {packagePath} -CertificatePath {pfxPath} -NonInteractive", + waitForExit: true, + timeOutInMilliseconds: 10000); + + // Assert + firstResult.Success.Should().BeFalse(); + firstResult.AllOutput.Should().Contain(string.Format(_invalidPasswordError, pfxPath)); + } + } + + [Fact] + public void SignCommand_SignPackageWithPfxFileWithNonInteractiveAndStdInPasswordFails() + { + // Arrange + var testLogger = new TestLogger(); + + using (var dir = TestDirectory.Create()) + using (var zipStream = new SimpleTestPackageContext().CreateAsStream()) + { + var packagePath = Path.Combine(dir, Guid.NewGuid().ToString()); + var pfxPath = Path.Combine(dir, Guid.NewGuid().ToString()); + + var password = Guid.NewGuid().ToString(); + var pfxBytes = _trustedTestCert.Source.Cert.Export(X509ContentType.Pfx, password); + + using (var fileStream = File.OpenWrite(pfxPath)) + using (var pfxStream = new MemoryStream(pfxBytes)) + { + pfxStream.CopyTo(fileStream); + } + + zipStream.Seek(offset: 0, loc: SeekOrigin.Begin); + + using (var fileStream = File.OpenWrite(packagePath)) + { + zipStream.CopyTo(fileStream); + } + + // Act + var firstResult = CommandRunner.Run( + _nugetExePath, + dir, + $"sign {packagePath} -CertificatePath {pfxPath} -NonInteractive", + waitForExit: true, + inputAction: (w) => + { + w.WriteLine(Guid.NewGuid().ToString()); + }, + timeOutInMilliseconds: 10000); + + // Assert + firstResult.Success.Should().BeFalse(); + firstResult.AllOutput.Should().Contain(string.Format(_invalidPasswordError, pfxPath)); } } }