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

Update verify command #1825

Merged
merged 8 commits into from
Dec 4, 2017
Merged

Conversation

PatoBeltran
Copy link
Contributor

@PatoBeltran PatoBeltran commented Nov 29, 2017

Fixes: NuGet/Home#6006

Fixes: NuGet/Home#6181
Fixes: NuGet/Home#6073
Fixes: NuGet/Home#6069
Refactored code, added missing verification steps, added more detailed
printing for verbose and debug mode, cleaned up strings

Also
Fixes: NuGet/Home#6212

Functional tests for verify flow are missing

@dnfclas
Copy link

dnfclas commented Nov 29, 2017

@PatoBeltran,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by .NET Foundation. We will now review your pull request.
Thanks,
.NET Foundation Pull Request Bot

@@ -5375,7 +5375,7 @@ nuget verify-Signatures .\*.nupkg</value>
<comment>Please don't localize this string</comment>
</data>
<data name="VerifyCommandSignaturesDescription" xml:space="preserve">
<value>Specifies the type of verification to be done. Currently only -Signatures verification is supported.</value>
<value>Specifies that a verification of the signature of the package(s) should be performed.</value>
Copy link
Member

Choose a reason for hiding this comment

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

Would this be more clear? Specifies that package signature verification should be performed

</data>
<data name="SignCommandOutputPath" xml:space="preserve">
<value>Signed package(s) output path: {0}</value>
<comment>0 - output directory path</comment>
</data>
<data name="Error_CommandFailedWithException" xml:space="preserve">
<value>NuGet Command failed. {0} threw exception: {1}</value>
Copy link
Member

Choose a reason for hiding this comment

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

I would avoid threw exception here, just print the message from the exception.

Copy link
Member

Choose a reason for hiding this comment

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

If this is debug only then the rule is: don't localize debug strings (if we ask someone to send us diag output we shouldn't have to translate it, and diag output isn't really for the user)

var packagesToVerify = LocalFolderUtility.ResolvePackageFromPath(verifyArgs.PackagePath);
LocalFolderUtility.EnsurePackageFileExists(verifyArgs.PackagePath, packagesToVerify);
var packagesToVerify = LocalFolderUtility.ResolvePackageFromPath(verifyArgs.PackagePath);
LocalFolderUtility.EnsurePackageFileExists(verifyArgs.PackagePath, packagesToVerify);
Copy link
Member

Choose a reason for hiding this comment

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

Glad to see these APIs being used here 🥇 🏆

catch (InvalidDataException e)
{
verifyArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.VerifyCommand_PackageIsNotValid, package));
verifyArgs.Logger.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.Error_CommandFailedWithException, nameof(VerifySignatureForPackageAsync), e.Message));
Copy link
Member

Choose a reason for hiding this comment

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

Don't localize this just log the full stack if it would be useful, also use ExceptionUtilities if it makes sense here

SignedPackageArchiveUtility.VerifySignedZipIntegrity(reader, hashAlgorithm, expectedHash);
if (!SignedPackageArchiveUtility.VerifySignedZipIntegrity(reader, hashAlgorithm, expectedHash))
{
throw new SignatureException(Strings.SignatureFailurePackageTampered);
Copy link
Member

Choose a reason for hiding this comment

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

Include the path if possible. This could come from checking if the stream is a FileStream and casting it to get the path. During a large restore with hundreds of packages it can be really confusing if there is a generic error message that doesn't specify the source.

{
return false;
}

for (var i = 0; i < byteSignature.Length; ++i)
{
var @byte = stream.ReadByte();
Copy link
Member

Choose a reason for hiding this comment

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

just use b for byte instead of an @

throw new Exception(Strings.ErrorByteSignatureTooBig);
}

while (stream.Position != 0)
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious how perf is for this. It seems like this should use a buffer and check within the buffer instead of going byte by byte. This can be profiled later, it shouldn't be changed in this PR

metadata.IsZip64 = true;
metadata.Zip64EndOfCentralDirectoryRecordPosition = reader.BaseStream.Position;
}
catch { }
Copy link
Member

Choose a reason for hiding this comment

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

Add a comment for why this is ignored

certStringBuilder.AppendLine($"{indentation}{string.Format(Strings.CertUtilityCertificateSubjectName, cert.Subject)}");
certStringBuilder.AppendLine($"{indentation}{string.Format(Strings.CertUtilityCertificateHash, cert.Thumbprint)}");
certStringBuilder.AppendLine($"{indentation}{string.Format(Strings.CertUtilityCertificateIssuer, cert.IssuerName.Name)}");
certStringBuilder.AppendLine($"{indentation}{string.Format(Strings.CertUtilityCertificateValidity, cert.NotBefore, cert.NotAfter)}");
Copy link
Member

Choose a reason for hiding this comment

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

String format needs to use CurrentCulture for user facing strings (this applies everywhere)

@@ -51,7 +51,7 @@ private static void X509Certificate2ToString(X509Certificate2 cert, StringBuilde
/// </summary>
/// <param name="certCollection">X509Certificate2Collection to be converted to string.</param>
/// <returns>string representation of the X509Certificate2Collection.</returns>
public static string X509Certificate2CollectionToString(X509Certificate2Collection certCollection)
public static string X509Certificate2CollectionToString(X509Certificate2Collection certCollection, string indentation = "")
Copy link
Member

Choose a reason for hiding this comment

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

avoid default parameters like this (nuget coding guideline)


static class SignatureTypeMethods
{
public static string GetString(this SignatureType type)
Copy link
Member

Choose a reason for hiding this comment

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

Does type.ToString() work? I believe there is a way to let .NET handle this

</data>
<data name="SignCommandOutputPath" xml:space="preserve">
<value>Signed package(s) output path: {0}</value>
<comment>0 - output directory path</comment>
</data>
<data name="Error_CommandFailedWithException" xml:space="preserve">
<value>NuGet Command failed. {0} threw exception: {1}</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

Lower case "command".

<value>Finished with {0} errors and {1} warnings.</value>
</data>
<data name="VerifyCommand_Success" xml:space="preserve">
<value>Successfully verified package.</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

package(s)

if (verifyArgs.Type != VerifyArgs.VerificationType.Signatures)
var executedSuccessfully = false;

if (ShouldExecuteType(verifyArgs, VerificationType.Signatures))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would read more clearly if you removed Type everywhere (e.g.: VerificationType, ShouldExecuteType, etc.). This could be simply ShouldExecuteVerification.


var trustProviders = SignatureVerificationProviderFactory.GetSignatureVerificationProviders();
var verifier = new PackageSignatureVerifier(trustProviders, SignedPackageVerifierSettings.RequireSigned);
var trustProviders = SignatureVerificationProviderFactory.GetSignatureVerificationProviders();
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems this should be renamed toverificationProviders. If verification includes a trust check, no problem.

}
executedSuccessfully = errorCount == 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't need to be evaluated after each package verification and can instead be evaluated once near the return statement below.


SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, zipMetadata.Zip64EndOfCentralDirectoryLocatorPosition + 16);
var relativeOffset = (uint)(reader.ReadUInt32() - metadata.SignatureFileEntryTotalSize);
Copy link
Contributor

Choose a reason for hiding this comment

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

relativeOffsetOfLocalFileHeader

@@ -51,7 +51,7 @@ private static void X509Certificate2ToString(X509Certificate2 cert, StringBuilde
/// </summary>
/// <param name="certCollection">X509Certificate2Collection to be converted to string.</param>
/// <returns>string representation of the X509Certificate2Collection.</returns>
public static string X509Certificate2CollectionToString(X509Certificate2Collection certCollection)
public static string X509Certificate2CollectionToString(X509Certificate2Collection certCollection, string indentation = "")
Copy link
Contributor

Choose a reason for hiding this comment

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

Update XML doc comments.

public static Signature Load(SignedCms cms)
{
var signerInfoCollection = cms.SignerInfos;
if (signerInfoCollection.Count != 1)
if (cms.SignerInfos.Count < 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

For the "root" signed CMS, the SignedData.signerInfos count MUST be 1.

/// <summary>
/// Create a signature based on a valid byte stream to be decoded as a signed cms
/// </summary>
/// <param name="stream">signature data</param>
public static Signature Load(Stream stream)
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a null check on stream.

{
return IsCertificateValid(certificate, out var chain, allowUntrustedRoot, checkRevocationStatus: true);
}

public static bool IsCertificatePublicKeyValid(X509Certificate2 certificate)
Copy link
Contributor

Choose a reason for hiding this comment

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

Add XML doc comments.

@PatoBeltran
Copy link
Contributor Author

@emgarten @dtivel addressed CR

Refactored code, added missing verification steps, added more detailed
printing for verbose and debug mode, cleaned up strings
@PatoBeltran PatoBeltran force-pushed the dev-patobeltran-verify branch 2 times, most recently from 2d1b1b2 to 6bb36fe Compare December 1, 2017 22:09
Copy link
Member

@emgarten emgarten left a comment

Choose a reason for hiding this comment

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

🚀

SignedPackageArchiveUtility.VerifySignedZipIntegrity(reader, hashAlgorithm, expectedHash);
if (!SignedPackageArchiveUtility.VerifySignedZipIntegrity(reader, hashAlgorithm, expectedHash))
{
throw new SignatureException(Strings.SignatureFailurePackageTampered, GetIdentity());
Copy link
Contributor

Choose a reason for hiding this comment

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

"Package integrity check failed." seems like a better message.

}

// everything to UTC
var timestampUpperGenTimeUtcTicks = tstInfoGenTime.AddTicks(tstInfoAccuracyInTicks).UtcTicks;
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to convert into ticks all over the place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

should using milliseconds be better?

Copy link
Contributor

Choose a reason for hiding this comment

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

You can compare DateTimeOffset objects. tstInfo.Timestamp is one.

            var tstInfoGenTime = tstInfo.Timestamp;
            var tstInfoAccuracy = tstInfo.AccuracyInMicroseconds;
            long tstInfoAccuracyInSeconds;

            if (tstInfoAccuracy.HasValue)
            {
                tstInfoAccuracyInSeconds = Math.Abs(tstInfoAccuracy.Value) / 1000000;
            }
            else if (string.Equals(tstInfo.PolicyId, Oids.BaselineTimestampPolicyOid))
            {
                tstInfoAccuracyInSeconds = 1;
            }
            else
            {
                tstInfoAccuracyInSeconds = 0;
            }

            // everything to UTC
            var timestampUpperGenTimeUtc = tstInfoGenTime.AddSeconds(tstInfoAccuracyInSeconds);
            var timestampLowerGenTimeUtc = tstInfoGenTime.AddSeconds(-tstInfoAccuracyInSeconds);
            DateTimeOffset signerCertExpiryUtc = signerCertificate.NotAfter.ToUniversalTime();
            DateTimeOffset signerCertBeginUtc = signerCertificate.NotBefore.ToUniversalTime();

            return timestampUpperGenTimeUtcTicks < signerCertExpiryUtcTicks &&
                timestampLowerGenTimeUtcTicks > signerCertBeginUtcTicks;


var result = SignatureVerificationStatus.Invalid;

using (var chain = new X509Chain(useMachineContext: true))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be false based on last discussion with you and Zhi. You can just use X509Chain's default constructor which already passes false.

</data>
<data name="SignCommandOutputPath" xml:space="preserve">
<value>Signed package(s) output path: {0}</value>
<comment>0 - output directory path</comment>
</data>
<data name="VerifyCommand_FinishedWithErrors" xml:space="preserve">
<value>Finished with {0} errors and {1} warnings.</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a comment indicating what the arguments to {0} and {1} are.

<value>Successfully verified package(s).</value>
</data>
<data name="VerifyCommand_VerifyingPackage" xml:space="preserve">
<value>Verifying {0}</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a comment indicating what an argument to {0} represents.

var errors = logMessages.Where(m => m.Level == LogLevel.Error).Count();
var warnings = logMessages.Where(m => m.Level == LogLevel.Warning).Count();
var errors = logMessages.Where(m => m.Level == LogLevel.Error).Count();
var warnings = logMessages.Where(m => m.Level == LogLevel.Warning).Count();
Copy link
Contributor

Choose a reason for hiding this comment

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

You can replace Where(...) with Count(...):

var errors = logMessages.Count(m => m.Level == LogLevel.Error); 
var warnings = logMessages.Count(m => m.Level == LogLevel.Warning); 

@@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using System.Diagnostics;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can undo?

@@ -60,7 +61,7 @@ public static string X509Certificate2CollectionToString(X509Certificate2Collecti
for (var i = 0; i < Math.Min(_limit, certCollection.Count); i++)
{
var cert = certCollection[i];
X509Certificate2ToString(cert, collectionStringBuilder);
X509Certificate2ToString(cert, collectionStringBuilder, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

indentation: ""


byte[] timestampByteArray;

using (var timestampNativeCms = NativeCms.Decode(timestampCms.Encode(), detached: false))
using (var timestampCertChain = new X509Chain(useMachineContext: true))
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the default constructor: new X509Chain().

var policy = timestampCertChain.ChainPolicy;

policy.ApplicationPolicy.Add(new Oid(Oids.TimeStampingEkuOid));
policy.VerificationFlags = X509VerificationFlags.IgnoreNotTimeValid;
Copy link
Contributor

Choose a reason for hiding this comment

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

Great, but we should probably fail if:

DateTime.NowUtc < timestampSignerCertificate.NotBefore

The certificate's validity date would be in the future.

@PatoBeltran
Copy link
Contributor Author

@dtivel addressed comments

}

// everything to UTC
var timestampUpperGenTimeUtcTicks = tstInfoGenTime.AddTicks(tstInfoAccuracyInTicks).UtcTicks;
Copy link
Contributor

Choose a reason for hiding this comment

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

You can compare DateTimeOffset objects. tstInfo.Timestamp is one.

            var tstInfoGenTime = tstInfo.Timestamp;
            var tstInfoAccuracy = tstInfo.AccuracyInMicroseconds;
            long tstInfoAccuracyInSeconds;

            if (tstInfoAccuracy.HasValue)
            {
                tstInfoAccuracyInSeconds = Math.Abs(tstInfoAccuracy.Value) / 1000000;
            }
            else if (string.Equals(tstInfo.PolicyId, Oids.BaselineTimestampPolicyOid))
            {
                tstInfoAccuracyInSeconds = 1;
            }
            else
            {
                tstInfoAccuracyInSeconds = 0;
            }

            // everything to UTC
            var timestampUpperGenTimeUtc = tstInfoGenTime.AddSeconds(tstInfoAccuracyInSeconds);
            var timestampLowerGenTimeUtc = tstInfoGenTime.AddSeconds(-tstInfoAccuracyInSeconds);
            DateTimeOffset signerCertExpiryUtc = signerCertificate.NotAfter.ToUniversalTime();
            DateTimeOffset signerCertBeginUtc = signerCertificate.NotBefore.ToUniversalTime();

            return timestampUpperGenTimeUtcTicks < signerCertExpiryUtcTicks &&
                timestampLowerGenTimeUtcTicks > signerCertBeginUtcTicks;

}

// everything to UTC
var timestampUpperGenTimeUtcTicks = tstInfoGenTime.AddMilliseconds(accuracyInMilliseconds);
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove Ticks from all these names.

@PatoBeltran PatoBeltran merged commit 74b4f8c into dev-feature-signing Dec 4, 2017
@PatoBeltran PatoBeltran deleted the dev-patobeltran-verify branch December 4, 2017 22:32
@clairernovotny
Copy link

Running this code throws for me:

This is throwing trying to load the timestampedSignature.

image

@mishra14
Copy link
Contributor

mishra14 commented Dec 6, 2017

@onovotny it seems that the data timestamped SignedCms is not being loaded correctly by the underlying timestamp implementation. What is the data and the source?

@clairernovotny
Copy link

@mishra14 this was fixed by @dtivel in a different PR. There were some issues in the code.

@mishra14
Copy link
Contributor

mishra14 commented Dec 6, 2017

@onovotny glad to know! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants