Skip to content
This repository was archived by the owner on Jul 30, 2024. It is now read-only.

Commit 49e608b

Browse files
committed
Allow unavailable revocation information during extract and validate (#314)
Address NuGet/Engineering#785
1 parent 5a88f20 commit 49e608b

11 files changed

Lines changed: 174 additions & 30 deletions

src/Validation.PackageSigning.ExtractAndValidateSignature/PackageSignatureVerifierFactory.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ public static IPackageSignatureVerifier CreateMinimal()
2525
allowUnsigned: true,
2626
allowUntrusted: false, // Invalid format of the signature uses this flag to determine success.
2727
allowIgnoreTimestamp: true,
28-
failWithMultipleTimestamps: false,
29-
allowNoTimestamp: true);
28+
allowMultipleTimestamps: true,
29+
allowNoTimestamp: true,
30+
allowUnknownRevocation: true);
3031

3132
return new PackageSignatureVerifier(
3233
verificationProviders,
@@ -48,8 +49,9 @@ public static IPackageSignatureVerifier CreateFull()
4849
allowUnsigned: false,
4950
allowUntrusted: false,
5051
allowIgnoreTimestamp: false,
51-
failWithMultipleTimestamps: true,
52-
allowNoTimestamp: false);
52+
allowMultipleTimestamps: false,
53+
allowNoTimestamp: false,
54+
allowUnknownRevocation: true);
5355

5456
return new PackageSignatureVerifier(
5557
verificationProviders,

src/Validation.PackageSigning.ExtractAndValidateSignature/SignatureValidator.cs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ namespace NuGet.Jobs.Validation.PackageSigning.ExtractAndValidateSignature
2020
{
2121
public class SignatureValidator : ISignatureValidator
2222
{
23+
private const string FormatVerificationName = "format verification";
24+
private const string SignatureVerificationName = "signature integrity and trust verification";
25+
2326
private readonly IPackageSigningStateService _packageSigningStateService;
2427
private readonly IPackageSignatureVerifier _minimalPackageSignatureVerifier;
2528
private readonly IPackageSignatureVerifier _fullPackageSignatureVerifier;
@@ -87,6 +90,7 @@ private async Task<SignatureValidatorResult> HandleSignedPackageAsync(
8790
// First, detect format errors with a minimal verification. This doesn't even check package integrity. The
8891
// minimal verification is expected to swallow any sort of signature format exception.
8992
var invalidFormatResult = await GetVerifyResult(
93+
FormatVerificationName,
9094
_minimalPackageSignatureVerifier,
9195
packageKey,
9296
signedPackageReader,
@@ -163,6 +167,7 @@ private async Task<SignatureValidatorResult> HandleSignedPackageAsync(
163167

164168
// Call the "verify" API, which does the main logic of signature validation.
165169
var failureResult = await GetVerifyResult(
170+
SignatureVerificationName,
166171
_fullPackageSignatureVerifier,
167172
packageKey,
168173
signedPackageReader,
@@ -205,6 +210,7 @@ private async Task<SignatureValidatorResult> HandleSignedPackageAsync(
205210
}
206211

207212
private async Task<SignatureValidatorResult> GetVerifyResult(
213+
string verificationName,
208214
IPackageSignatureVerifier verifier,
209215
int packageKey,
210216
ISignedPackageReader signedPackageReader,
@@ -214,26 +220,27 @@ private async Task<SignatureValidatorResult> GetVerifyResult(
214220
var verifyResult = await verifier.VerifySignaturesAsync(
215221
signedPackageReader,
216222
cancellationToken);
217-
if (!verifyResult.Valid)
218-
{
219-
var errorIssues = verifyResult
220-
.Results
221-
.SelectMany(x => x.GetErrorIssues())
222-
.ToList();
223223

224-
var errorsForLogs = errorIssues
225-
.Select(x => $"{x.Code}: {x.Message}")
226-
.ToList();
227-
var warningsForLogs = verifyResult
228-
.Results
229-
.SelectMany(x => x.GetWarningIssues())
230-
.Select(x => $"{x.Code}: {x.Message}")
231-
.ToList();
224+
var errorIssues = verifyResult
225+
.Results
226+
.SelectMany(x => x.GetErrorIssues())
227+
.ToList();
228+
var warningsForLogs = verifyResult
229+
.Results
230+
.SelectMany(x => x.GetWarningIssues())
231+
.Select(x => $"{x.Code}: {x.Message}")
232+
.ToList();
233+
var errorsForLogs = errorIssues
234+
.Select(x => $"{x.Code}: {x.Message}")
235+
.ToList();
232236

237+
if (!verifyResult.Valid)
238+
{
233239
_logger.LogInformation(
234-
"Signed package {PackageId} {PackageVersion} is blocked for validation {ValidationId} due to verify failures. Errors: {Errors} Warnings: {Warnings}",
240+
"Signed package {PackageId} {PackageVersion} is blocked during {VerificationName} for validation {ValidationId} . Errors: [{Errors}] Warnings: [{Warnings}]",
235241
message.PackageId,
236242
message.PackageVersion,
243+
verificationName,
237244
message.ValidationId,
238245
errorsForLogs,
239246
warningsForLogs);
@@ -260,8 +267,19 @@ private async Task<SignatureValidatorResult> GetVerifyResult(
260267
message,
261268
errorValidationIssues);
262269
}
270+
else
271+
{
272+
_logger.LogInformation(
273+
"Signed package {PackageId} {PackageVersion} passed {VerificationName} for validation {ValidationId}. Errors: [{Errors}] Warnings: [{Warnings}]",
274+
message.PackageId,
275+
message.PackageVersion,
276+
verificationName,
277+
message.ValidationId,
278+
errorsForLogs,
279+
warningsForLogs);
263280

264-
return null;
281+
return null;
282+
}
265283
}
266284

267285
private async Task<SignatureValidatorResult> RejectAsync(

src/Validation.PackageSigning.ExtractAndValidateSignature/Validation.PackageSigning.ExtractAndValidateSignature.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
<Version>1.1.2</Version>
107107
</PackageReference>
108108
<PackageReference Include="NuGet.Packaging">
109-
<Version>4.7.0-preview1-4853</Version>
109+
<Version>4.7.0-preview1-4870</Version>
110110
</PackageReference>
111111
<PackageReference Include="NuGet.Services.Configuration">
112112
<Version>2.12.0</Version>

tests/Validation.PackageSigning.ExtractAndValidateSignature.Tests/SignaturePartsExtractorFacts.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class SignaturePartsExtractorFacts
2626
private const string BouncyCastleCollection = "Collection";
2727

2828
private static readonly DateTime Leaf1TimestampValue = DateTime
29-
.Parse("2018-01-16T20:31:09.0000000Z")
29+
.Parse("2018-01-26T22:08:31.0000000Z")
3030
.ToUniversalTime();
3131

3232
/// <summary>
@@ -42,7 +42,7 @@ public class SignaturePartsExtractorFacts
4242
{
4343
new SubjectAndThumbprint(
4444
"CN=NUGET_DO_NOT_TRUST.intermediate.test.test, OU=Test Organizational Unit Name, O=Test Organization Name, L=Redmond, S=WA, C=US",
45-
"d5949445cde4d80bc0c857dddb8520114a146d73de081a77404b0c17dda6a4b4"),
45+
"7358e4597696b1d02e7aa2b3cf30a7cf154f2c8ff0710fd0dc3ace17e3784054"),
4646
new SubjectAndThumbprint(
4747
"CN=NUGET_DO_NOT_TRUST.root.test.test, OU=Test Organizational Unit Name, O=Test Organization Name, L=Redmond, S=WA, C=US",
4848
TestResources.RootThumbprint),

tests/Validation.PackageSigning.ExtractAndValidateSignature.Tests/SignatureValidatorIntegrationTests.cs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class SignatureValidatorIntegrationTests : IDisposable
3939
private readonly List<string> _trustedThumbprints;
4040
private readonly IPackageSignatureVerifier _minimalPackageSignatureVerifier;
4141
private readonly IPackageSignatureVerifier _fullPackageSignatureVerifier;
42-
private readonly ILogger<SignatureValidator> _logger;
42+
private readonly RecordingLogger<SignatureValidator> _logger;
4343
private readonly int _packageKey;
4444
private SignedPackageArchive _package;
4545
private readonly SignatureValidationMessage _message;
@@ -69,7 +69,7 @@ public SignatureValidatorIntegrationTests(CertificateIntegrationTestFixture fixt
6969

7070
var loggerFactory = new LoggerFactory();
7171
loggerFactory.AddXunit(output);
72-
_logger = loggerFactory.CreateLogger<SignatureValidator>();
72+
_logger = new RecordingLogger<SignatureValidator>(loggerFactory.CreateLogger<SignatureValidator>());
7373

7474
// Initialize data.
7575
_packageKey = 23;
@@ -120,6 +120,64 @@ public async Task AcceptsValidSignedPackage()
120120
Assert.Empty(result.Issues);
121121
}
122122

123+
[Fact]
124+
public async Task RejectsUntrustedSigningCertificate()
125+
{
126+
// Arrange
127+
AllowCertificateThumbprint(TestResources.Leaf1Thumbprint);
128+
_package = TestResources.SignedPackageLeaf1Reader;
129+
130+
// Act
131+
var result = await _target.ValidateAsync(
132+
_packageKey,
133+
_package,
134+
_message,
135+
_token);
136+
137+
// Assert
138+
VerifyPackageSigningStatus(result, ValidationStatus.Failed, PackageSigningStatus.Invalid);
139+
Assert.NotEmpty(result.Issues);
140+
var clientIssues = result
141+
.Issues
142+
.OfType<ClientSigningVerificationFailure>()
143+
.Where(x => x.ClientCode == "NU3021")
144+
.ToList();
145+
var untrustedIssue = Assert.Single(clientIssues);
146+
Assert.Equal(
147+
"A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.",
148+
untrustedIssue.ClientMessage);
149+
}
150+
151+
[Fact]
152+
public async Task AcceptsTrustedCertificateWithUnavailableRevocation()
153+
{
154+
// Arrange
155+
using (var trustedTestCert = new TrustedTestCert<X509Certificate2>(
156+
await TestResources.GetTestRootCertificateAsync(),
157+
x => x,
158+
StoreName.Root,
159+
StoreLocation.LocalMachine,
160+
maximumValidityPeriod: TimeSpan.MaxValue))
161+
{
162+
_package = TestResources.SignedPackageLeaf1Reader;
163+
AllowCertificateThumbprint(TestResources.Leaf1Thumbprint);
164+
165+
// Act
166+
var result = await _target.ValidateAsync(
167+
_packageKey,
168+
_package,
169+
_message,
170+
_token);
171+
172+
// Assert
173+
VerifyPackageSigningStatus(result, ValidationStatus.Succeeded, PackageSigningStatus.Valid);
174+
Assert.Empty(result.Issues);
175+
var allMessages = string.Join(Environment.NewLine, _logger.Messages);
176+
Assert.Contains("NU3018: The revocation function was unable to check revocation because the revocation server was offline.", allMessages);
177+
Assert.Contains("NU3018: The revocation function was unable to check revocation for the certificate.", allMessages);
178+
}
179+
}
180+
123181
[Fact]
124182
public async Task RejectsPackageWithAddedFile()
125183
{

tests/Validation.PackageSigning.ExtractAndValidateSignature.Tests/Support/CertificateIntegrationTestFixture.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ await GetSignedPackageStreamAsync(reference, resourceName, certificate, output),
111111

112112
private static async Task<byte[]> GenerateSignedPackageBytesAsync(string resourceName, TrustedTestCert<TestCertificate> certificate, ITestOutputHelper output)
113113
{
114+
if (string.IsNullOrWhiteSpace(_testTimestampServer))
115+
{
116+
Assert.False(
117+
string.IsNullOrWhiteSpace(_testTimestampServer),
118+
"You must set a TIMESTAMP_SERVER_URL environment variable to an accessible timestamping authority URL.");
119+
}
120+
114121
var testLogger = new TestLogger(output);
115122
var timestampProvider = new Rfc3161TimestampProvider(new Uri(_testTimestampServer));
116123
var signatureProvider = new X509SignatureProvider(timestampProvider);
@@ -125,7 +132,7 @@ private static async Task<byte[]> GenerateSignedPackageBytesAsync(string resourc
125132
signatureProvider,
126133
x =>
127134
{
128-
var request = new SignPackageRequest(certificate.TrustedCert, HashAlgorithmName.SHA256);
135+
var request = new AuthorSignPackageRequest(certificate.TrustedCert, HashAlgorithmName.SHA256);
129136
return x.SignAsync(request, testLogger, CancellationToken.None);
130137
});
131138

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Validation.PackageSigning.ExtractAndValidateSignature.Tests
9+
{
10+
public class RecordingLogger<T> : ILogger<T>
11+
{
12+
private readonly ILogger<T> _inner;
13+
private readonly List<string> _messages = new List<string>();
14+
15+
public RecordingLogger(ILogger<T> inner)
16+
{
17+
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
18+
}
19+
20+
public IReadOnlyList<string> Messages => _messages;
21+
22+
public IDisposable BeginScope<TState>(TState state)
23+
{
24+
return _inner.BeginScope(state);
25+
}
26+
27+
public bool IsEnabled(LogLevel logLevel)
28+
{
29+
return _inner.IsEnabled(logLevel);
30+
}
31+
32+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
33+
{
34+
_messages.Add(formatter(state, exception));
35+
_inner.Log(logLevel, eventId, state, exception, formatter);
36+
}
37+
}
38+
}

tests/Validation.PackageSigning.ExtractAndValidateSignature.Tests/Support/TestResources.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.IO;
6+
using System.Linq;
7+
using System.Security.Cryptography.X509Certificates;
8+
using System.Threading;
9+
using System.Threading.Tasks;
510
using NuGet.Packaging.Signing;
611

712
namespace Validation.PackageSigning.ExtractAndValidateSignature.Tests
813
{
914
public static class TestResources
1015
{
1116
private const string ResourceNamespace = "Validation.PackageSigning.ExtractAndValidateSignature.Tests.TestData";
17+
private static readonly Lazy<Task<byte[]>> _lazyTestRootCertificate = new Lazy<Task<byte[]>>(async () =>
18+
{
19+
using (var package = SignedPackageLeaf1Reader)
20+
{
21+
var signature = await package.GetSignatureAsync(CancellationToken.None);
22+
var certificates = SignatureUtility.GetPrimarySignatureCertificates(signature);
23+
return certificates.Last().RawData;
24+
}
25+
});
1226

1327
public const string SignedPackageLeaf1 = ResourceNamespace + ".TestSigned.leaf-1.1.0.0.nupkg";
1428
public const string SignedPackageLeaf2 = ResourceNamespace + ".TestSigned.leaf-2.2.0.0.nupkg";
@@ -18,17 +32,17 @@ public static class TestResources
1832
/// <summary>
1933
/// This is the SHA-256 thumbprint of the root CA certificate for the signing certificate of <see cref="SignedPackageLeaf1"/>.
2034
/// </summary>
21-
public const string RootThumbprint = "0e829fa17cfd9be513a41d9f205320f7d035f48d6c4cc7acbaa95f1744c1d6bb";
35+
public const string RootThumbprint = "557276839c961df211cf267b318d880568676efa41e8b62d9bb38752c1d6214d";
2236

2337
/// <summary>
2438
/// This is the SHA-256 thumbprint of the signing certificate in <see cref="SignedPackageLeaf1"/>.
2539
/// </summary>
26-
public const string Leaf1Thumbprint = "56a23ed7c0ef80bd0269d4a3b41e3e2830243a9fc85594b6c311e27423df6023";
40+
public const string Leaf1Thumbprint = "4456d5d38709876dcd20ef3d7ba98bfd79fcaee91141d153a55f10841ef909c6";
2741

2842
/// <summary>
2943
/// This is the SHA-256 thumbprint of the signing certificate in <see cref="SignedPackageLeaf2"/>.
3044
/// </summary>
31-
public const string Leaf2Thumbprint = "cd177f02cb88f6e6fb6b0dd67d68559b101c3e100fb19ebf4db43d9d082674e1";
45+
public const string Leaf2Thumbprint = "a8cc70dbbd8bc61410231805b690cca7c5a8d07553c1c49b299a6aabaeb7ff9a";
3246

3347
/// <summary>
3448
/// This is the SHA-256 thumbprint of the timestamp certificate in <see cref="SignedPackageLeaf1"/>.
@@ -38,6 +52,12 @@ public static class TestResources
3852
public static SignedPackageArchive SignedPackageLeaf1Reader => LoadPackage(SignedPackageLeaf1);
3953
public static SignedPackageArchive SignedPackageLeaf2Reader => LoadPackage(SignedPackageLeaf2);
4054

55+
public static async Task<X509Certificate2> GetTestRootCertificateAsync()
56+
{
57+
var bytes = await _lazyTestRootCertificate.Value;
58+
return new X509Certificate2((byte[])bytes.Clone());
59+
}
60+
4161
/// <summary>
4262
/// Buffer the resource stream into memory so the caller doesn't have to dispose.
4363
/// </summary>

0 commit comments

Comments
 (0)