Skip to content

Commit

Permalink
Add support to allow users to input password through commandline to s…
Browse files Browse the repository at this point in the history
…ign command (#1824)

## Bug
Fixes: NuGet/Home#5904
Regression: No

## Fix
Details: This PR adds support in sign command to allow users to interactively type pfx file password securely. To allow this I have added  `IPasswordProvider.cs` and its implementation `ConsolePasswordProvider.cs` that allows `SignCommandRunner` to read password from console in `-NonInteractive` mode. The underlying implementation comes from `IConsole.ReadSecureString`.

I have also added some tests using pfx files and done some cleanup in the test code.

## Testing/Validation
Tests Added: Yes
Validation done:  manual testing with pfx files.
  • Loading branch information
Ankit Mishra authored Nov 29, 2017
1 parent 427fc2c commit 065262c
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 46 deletions.
3 changes: 2 additions & 1 deletion src/NuGet.Clients/NuGet.CommandLine/Commands/SignCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public SignArgs GetSignArgs()
Overwrite = Overwrite,
NonInteractive = NonInteractive,
Timestamper = Timestamper,
TimestampHashAlgorithm = timestampHashAlgorithm
TimestampHashAlgorithm = timestampHashAlgorithm,
PasswordProvider = new ConsolePasswordProvider(Console)
};
}

Expand Down
46 changes: 46 additions & 0 deletions src/NuGet.Clients/NuGet.CommandLine/ConsolePasswordProvider.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Allows requesting a user to input their password through Console.
/// </summary>
internal class ConsolePasswordProvider : IPasswordProvider
{
private IConsole _console;

public ConsolePasswordProvider(IConsole console)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
}

#if IS_DESKTOP
/// <summary>
/// Requests user to input password and returns it as a SecureString on Console.
/// </summary>
/// <param name="filePath">Path to the file that needs a password to open.</param>
/// <param name="token">Cancellation token.</param>
/// <returns>SecureString containing the user input password. The SecureString should be disposed after use.</returns>
public Task<SecureString> 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
}
}
20 changes: 19 additions & 1 deletion src/NuGet.Clients/NuGet.CommandLine/NuGetResources.Designer.cs

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

7 changes: 7 additions & 0 deletions src/NuGet.Clients/NuGet.CommandLine/NuGetResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6151,4 +6151,11 @@ Oluşturma sırasında NuGet'in paketleri indirmesini önlemek için, Visual Stu
<data name="Error_ResponseFileTooLarge" xml:space="preserve">
<value>Response file '{0}' cannot be larger than {1}mb</value>
</data>
<data name="ConsolePasswordProvider_DisplayFile" xml:space="preserve">
<value>Please provide password for: {0}</value>
<comment>0 - file that requires password</comment>
</data>
<data name="ConsolePasswordProvider_PromptForPassword" xml:space="preserve">
<value>Password: </value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -43,5 +46,20 @@ internal class CertificateSourceOptions
/// </summary>
public string Fingerprint { get; set; }

/// <summary>
/// bool used to indicate if the user can be prompted for password.
/// </summary>
public bool NonInteractive { get; set; }

/// <summary>
/// Password provider to get the password from user for opening a pfx file.
/// </summary>
public IPasswordProvider PasswordProvider { get; set; }

/// <summary>
/// Cancellation token.
/// </summary>
public CancellationToken Token { get; set; }

}
}
82 changes: 61 additions & 21 deletions src/NuGet.Core/NuGet.Commands/SignCommand/CertificateProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -32,24 +36,15 @@ internal static class CertificateProvider
/// <param name="options">CertificateSourceOptions to be used while searching for the certificates.</param>
/// <returns>An X509Certificate2Collection object containing matching certificates.
/// If no matching certificates are found then it returns an empty collection.</returns>
public static X509Certificate2Collection GetCertificates(CertificateSourceOptions options)
public static async Task<X509Certificate2Collection> GetCertificatesAsync(CertificateSourceOptions options)
{
// check certificate path
var resultCollection = new X509Certificate2Collection();
if (!string.IsNullOrEmpty(options.CertificatePath))
{
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);
}
Expand Down Expand Up @@ -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<X509Certificate2> 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;
}

Expand Down
24 changes: 24 additions & 0 deletions src/NuGet.Core/NuGet.Commands/SignCommand/IPasswordProvider.cs
Original file line number Diff line number Diff line change
@@ -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
/// <summary>
/// Requests user to input password and returns it as a SecureString.
/// </summary>
/// <param name="filePath">Path to the file that needs a password to open.</param>
/// <param name="token">Cancellation token.</param>
/// <returns>SecureString containing the user input password. The SecureString should be disposed after use.</returns>
Task<SecureString> GetPassword(string filePath, CancellationToken token);
#endif
}
}
7 changes: 6 additions & 1 deletion src/NuGet.Core/NuGet.Commands/SignCommand/SignArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -94,6 +94,11 @@ public class SignArgs
/// </summary>
public ILogger Logger { get; set; }

/// <summary>
/// Password provider to get the password from user for opening a pfx file.
/// </summary>
public IPasswordProvider PasswordProvider { get; set; }

/// <summary>
/// Cancellation Token.
/// </summary>
Expand Down
11 changes: 7 additions & 4 deletions src/NuGet.Core/NuGet.Commands/SignCommand/SignCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public async Task<int> 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,
Expand Down Expand Up @@ -174,7 +174,7 @@ private SignPackageRequest GenerateSignPackageRequest(SignArgs signArgs, X509Cer
};
}

private static X509Certificate2 GetCertificate(SignArgs signArgs)
private static async Task<X509Certificate2> GetCertificateAsync(SignArgs signArgs)
{
var certFindOptions = new CertificateSourceOptions()
{
Expand All @@ -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)
{
Expand Down
Loading

0 comments on commit 065262c

Please sign in to comment.