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

Add support to allow users to input password through commandline to sign command #1824

Merged
merged 4 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from 3 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
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