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

Commit 02cbfdd

Browse files
authored
[Package Signing] Integrate the ICertificateVerifier (#331)
Integrates the `ICertificateVerifier` to the Validate Certificate job. The `ICertificateVerifier`'s implementation depends on #309.
1 parent 500e467 commit 02cbfdd

9 files changed

Lines changed: 507 additions & 63 deletions

src/Validation.PackageSigning.ValidateCertificate/CertificateValidationMessageHandler.cs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Security.Cryptography.X509Certificates;
58
using System.Threading;
69
using System.Threading.Tasks;
710
using Microsoft.Extensions.Logging;
@@ -20,18 +23,21 @@ public sealed class CertificateValidationMessageHandler : IMessageHandler<Certif
2023
{
2124
private readonly ICertificateStore _certificateStore;
2225
private readonly ICertificateValidationService _certificateValidationService;
26+
private readonly ICertificateVerifier _certificateVerifier;
2327
private readonly ILogger<CertificateValidationMessageHandler> _logger;
2428

2529
private readonly int _maximumValidationFailures;
2630

2731
public CertificateValidationMessageHandler(
2832
ICertificateStore certificateStore,
2933
ICertificateValidationService certificateValidationService,
34+
ICertificateVerifier certificateVerifier,
3035
ILogger<CertificateValidationMessageHandler> logger,
3136
int maximumValidationFailures = CertificateValidationService.DefaultMaximumValidationFailures)
3237
{
3338
_certificateStore = certificateStore ?? throw new ArgumentNullException(nameof(certificateStore));
3439
_certificateValidationService = certificateValidationService ?? throw new ArgumentNullException(nameof(certificateValidationService));
40+
_certificateVerifier = certificateVerifier ?? throw new ArgumentNullException(nameof(certificateVerifier));
3541
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3642

3743
_maximumValidationFailures = maximumValidationFailures;
@@ -95,11 +101,28 @@ public async Task<bool> HandleAsync(CertificateValidationMessage message)
95101
}
96102
}
97103

98-
// Download and verify the certificate.
99-
// TODO: Download all parent certificates and pass them to verification "VerifyAsync",
100-
// will be done as part of: https://github.com/nuget/engineering/issues/787
101-
var certificate = await _certificateStore.LoadAsync(validation.EndCertificate.Thumbprint, CancellationToken.None);
102-
var result = await _certificateValidationService.VerifyAsync(certificate);
104+
CertificateVerificationResult result;
105+
106+
using (var certificates = await LoadCertificatesAsync(validation))
107+
{
108+
switch (validation.EndCertificate.Use)
109+
{
110+
case EndCertificateUse.CodeSigning:
111+
result = _certificateVerifier.VerifyCodeSigningCertificate(
112+
certificates.EndCertificate,
113+
certificates.AncestorCertificates);
114+
break;
115+
116+
case EndCertificateUse.Timestamping:
117+
result = _certificateVerifier.VerifyTimestampingCertificate(
118+
certificates.EndCertificate,
119+
certificates.AncestorCertificates);
120+
break;
121+
122+
default:
123+
throw new InvalidOperationException($"Unknown {nameof(EndCertificateUse)}: {validation.EndCertificate.Use}");
124+
}
125+
}
103126

104127
// Save the result. This may alert if packages are invalidated.
105128
if (!await _certificateValidationService.TrySaveResultAsync(validation, result))
@@ -157,5 +180,45 @@ private bool HasValidationCompleted(EndCertificateValidation validation, Certifi
157180

158181
throw new InvalidOperationException($"Unknown {nameof(EndCertificateStatus)} value: {result.Status}");
159182
}
183+
184+
private async Task<LoadCertificatesResult> LoadCertificatesAsync(EndCertificateValidation validation)
185+
{
186+
// Create a list of all the thumbprints that need to be downloaded. The first thumbprint is the end certificate,
187+
// the rest are the end certificate's ancestors.
188+
var thumbprints = new List<string>();
189+
190+
thumbprints.Add(validation.EndCertificate.Thumbprint);
191+
thumbprints.AddRange(validation.EndCertificate.CertificateChainLinks.Select(l => l.ParentCertificate.Thumbprint));
192+
193+
var certificates = await Task.WhenAll(thumbprints.Select(t => _certificateStore.LoadAsync(t, CancellationToken.None)));
194+
195+
return new LoadCertificatesResult(
196+
endCertificate: certificates.First(),
197+
ancestorCertificates: certificates.Skip(1).ToArray());
198+
}
199+
200+
private class LoadCertificatesResult : IDisposable
201+
{
202+
public LoadCertificatesResult(
203+
X509Certificate2 endCertificate,
204+
IReadOnlyList<X509Certificate2> ancestorCertificates)
205+
{
206+
EndCertificate = endCertificate ?? throw new ArgumentNullException(nameof(endCertificate));
207+
AncestorCertificates = ancestorCertificates ?? throw new ArgumentNullException(nameof(ancestorCertificates));
208+
}
209+
210+
public X509Certificate2 EndCertificate { get; }
211+
public IReadOnlyList<X509Certificate2> AncestorCertificates { get; }
212+
213+
public void Dispose()
214+
{
215+
EndCertificate.Dispose();
216+
217+
foreach (var ancestor in AncestorCertificates)
218+
{
219+
ancestor.Dispose();
220+
}
221+
}
222+
}
160223
}
161-
}
224+
}

src/Validation.PackageSigning.ValidateCertificate/CertificateValidationService.cs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,15 @@ public CertificateValidationService(
4343

4444
public Task<EndCertificateValidation> FindCertificateValidationAsync(CertificateValidationMessage message)
4545
{
46+
// Fetch the validation, the end certificate that this validation is for, and all of the parent
47+
// certificates that the end certificate depends on.
4648
return _context
4749
.CertificateValidations
4850
.Where(v => v.ValidationId == message.ValidationId && v.EndCertificateKey == message.CertificateKey)
49-
.Include(v => v.EndCertificate)
51+
.Include(v => v.EndCertificate.CertificateChainLinks.Select(l => l.ParentCertificate))
5052
.FirstOrDefaultAsync();
5153
}
5254

53-
public Task<CertificateVerificationResult> VerifyAsync(X509Certificate2 certificate)
54-
{
55-
// TODO: This will be implemented in a separate change!
56-
throw new NotImplementedException();
57-
}
58-
5955
public async Task<bool> TrySaveResultAsync(EndCertificateValidation validation, CertificateVerificationResult result)
6056
{
6157
if (validation.EndCertificate.Status == EndCertificateStatus.Revoked && result.Status != EndCertificateStatus.Revoked)
@@ -171,6 +167,7 @@ void InvalidateCertificate()
171167

172168
return ProcessDependentSignaturesAsync(
173169
validation.EndCertificate,
170+
result,
174171
invalidationDecider,
175172
onAllSignaturesHandled: InvalidateCertificate);
176173
}
@@ -193,6 +190,7 @@ void RevokeCertificate()
193190

194191
return ProcessDependentSignaturesAsync(
195192
validation.EndCertificate,
193+
result,
196194
invalidationDecider,
197195
onAllSignaturesHandled: RevokeCertificate);
198196
}
@@ -201,11 +199,13 @@ void RevokeCertificate()
201199
/// The helper that processes how a certificate's status change affects its dependent signatures.
202200
/// </summary>
203201
/// <param name="certificate">The certificate whose dependent signatures should be processed.</param>
202+
/// <param name="certificateVerificationResult">The result of the certificate's verification.</param>
204203
/// <param name="signatureDecider">The delegate that decides how a dependent signature should be handled.</param>
205204
/// <param name="onAllSignaturesHandled">The action that will be called once all dependent signatures have been processed.</param>
206205
/// <returns></returns>
207206
private async Task ProcessDependentSignaturesAsync(
208207
EndCertificate certificate,
208+
CertificateVerificationResult certificateVerificationResult,
209209
SignatureDecider signatureDecider,
210210
Action onAllSignaturesHandled)
211211
{
@@ -246,7 +246,7 @@ private async Task ProcessDependentSignaturesAsync(
246246
{
247247
var decision = signatureDecider(signature);
248248

249-
HandleSignatureDecision(signature, decision);
249+
HandleSignatureDecision(signature, decision, certificate, certificateVerificationResult);
250250
}
251251
}
252252
while (signatures.Count == MaxSignatureUpdatesPerTransaction);
@@ -294,7 +294,7 @@ private Task<List<PackageSignature>> FindSignaturesAsync(EndCertificate certific
294294
}
295295

296296
return packageSignatures
297-
.Include(s => s.TrustedTimestamps)
297+
.Include(s => s.TrustedTimestamps.Select(t => t.EndCertificate))
298298
.Include(s => s.PackageSigningState)
299299
.OrderBy(s => s.Key)
300300
.Skip(page * MaxSignatureUpdatesPerTransaction)
@@ -307,33 +307,72 @@ private Task<List<PackageSignature>> FindSignaturesAsync(EndCertificate certific
307307
/// Handle the decision on how to update the signature.
308308
/// </summary>
309309
/// <param name="signature">The signature that should be updated.</param>
310-
/// <param name="decision"></param>
310+
/// <param name="decision">How the signature should be updated.</param>
311+
/// <param name="certificate">The certificate that signature depends on that changed the signature's state.</param>
312+
/// <param name="certificateVerificationResult">The certificate verification that changed the signature's state.</param>
311313
private void HandleSignatureDecision(
312314
PackageSignature signature,
313-
SignatureDecision decision)
315+
SignatureDecision decision,
316+
EndCertificate certificate,
317+
CertificateVerificationResult certificateVerificationResult)
314318
{
315-
// TODO: Log all the necessary information to investigate the signature decision.
316319
switch (decision)
317320
{
318321
case SignatureDecision.Ignore:
322+
_logger.LogInformation(
323+
"Signature {SignatureKey} is not affected by certificate verification result: {CertificateVerificationResult}",
324+
signature.Key,
325+
certificateVerificationResult);
319326
break;
320327

321328
case SignatureDecision.Warn:
322-
signature.Status = PackageSignatureStatus.Invalid;
323-
signature.PackageSigningState.SigningStatus = PackageSigningStatus.Invalid;
329+
_logger.LogWarning(
330+
"Invalidating signature {SignatureKey} due to certificate verification result: {CertificateVerificationResult}",
331+
signature.Key,
332+
certificateVerificationResult);
333+
334+
InvalidateSignature(signature, certificate);
324335

325336
_telemetryService.TrackPackageSignatureMayBeInvalidatedEvent(signature);
337+
326338
break;
327339

328340
case SignatureDecision.Reject:
329-
signature.Status = PackageSignatureStatus.Invalid;
330-
signature.PackageSigningState.SigningStatus = PackageSigningStatus.Invalid;
341+
_logger.LogWarning(
342+
"Rejecting signature {SignatureKey} due to certificate verification result: {CertificateVerificationResult}",
343+
signature.Key,
344+
certificateVerificationResult);
345+
346+
InvalidateSignature(signature, certificate);
331347

332348
_telemetryService.TrackPackageSignatureShouldBeInvalidatedEvent(signature);
333349
break;
334350

335351
default:
336-
throw new InvalidOperationException($"Unknown signature decision: {decision}");
352+
throw new InvalidOperationException(
353+
$"Unknown signature decision '{decision}' for certificate verification result: {certificateVerificationResult}");
354+
}
355+
}
356+
357+
private void InvalidateSignature(PackageSignature signature, EndCertificate certificate)
358+
{
359+
signature.Status = PackageSignatureStatus.Invalid;
360+
signature.PackageSigningState.SigningStatus = PackageSigningStatus.Invalid;
361+
362+
if (certificate.Use == EndCertificateUse.Timestamping)
363+
{
364+
var affectedTimestamps = signature.TrustedTimestamps
365+
.Where(t => t.EndCertificate.Thumbprint == certificate.Thumbprint);
366+
367+
foreach (var timestamp in affectedTimestamps)
368+
{
369+
_logger.LogWarning(
370+
"Invalidating timestamp {TimestampKey} due to invalid certificate {CertificateKey}",
371+
signature.Key,
372+
certificate.Key);
373+
374+
timestamp.Status = TrustedTimestampStatus.Invalid;
375+
}
337376
}
338377
}
339378
}

src/Validation.PackageSigning.ValidateCertificate/ICertificateValidationService.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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.Security.Cryptography.X509Certificates;
54
using System.Threading.Tasks;
65
using NuGet.Jobs.Validation.PackageSigning.Messages;
76
using NuGet.Services.Validation;
@@ -17,14 +16,6 @@ public interface ICertificateValidationService
1716
/// <returns>The entity representing the certificate validation's state, or null if one could not be found.</returns>
1817
Task<EndCertificateValidation> FindCertificateValidationAsync(CertificateValidationMessage message);
1918

20-
/// <summary>
21-
/// Verify the certificate. Ensures the certificate is well-formed
22-
/// and does online revocation checking.
23-
/// </summary>
24-
/// <param name="certificate">The certificate to validate.</param>
25-
/// <returns>The result of the verification.</returns>
26-
Task<CertificateVerificationResult> VerifyAsync(X509Certificate2 certificate);
27-
2819
/// <summary>
2920
/// Update the requested <see cref="CertificateValidation"/> with the <see cref="CertificateVerificationResult"/>.
3021
/// This may kick off alerts if packages are invalidated!

src/Validation.PackageSigning.ValidateCertificate/ICertificateVerifier.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Security.Cryptography.X509Certificates;
67

78
namespace Validation.PackageSigning.ValidateCertificate
@@ -26,14 +27,14 @@ public interface ICertificateVerifier
2627
/// <param name="certificate">The certificate to verify.</param>
2728
/// <param name="extraCertificates">A collection of certificates that may be used to build the certificate chain.</param>
2829
/// <returns>The result of the verification.</returns>
29-
CertificateVerificationResult VerifyCodeSigningCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates);
30+
CertificateVerificationResult VerifyCodeSigningCertificate(X509Certificate2 certificate, IReadOnlyList<X509Certificate2> extraCertificates);
3031

3132
/// <summary>
3233
/// Determine the status of a timestamping <see cref="X509Certificate2"/>.
3334
/// </summary>
3435
/// <param name="certificate">The certificate to verify.</param>
3536
/// <param name="extraCertificates">A collection of certificates that may be used to build the certificate chain.</param>
3637
/// <returns>The result of the verification.</returns>
37-
CertificateVerificationResult VerifyTimestampingCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates);
38+
CertificateVerificationResult VerifyTimestampingCertificate(X509Certificate2 certificate, IReadOnlyList<X509Certificate2> extraCertificates);
3839
}
3940
}

src/Validation.PackageSigning.ValidateCertificate/OnlineCertificateVerifier.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Linq;
67
using System.Security.Cryptography;
78
using System.Security.Cryptography.X509Certificates;
@@ -44,17 +45,17 @@ public CertificateVerificationResult VerifyCertificate(X509Certificate2 certific
4445
return VerifyCertificate(certificate, extraCertificates, applicationPolicy: null);
4546
}
4647

47-
public CertificateVerificationResult VerifyCodeSigningCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates)
48+
public CertificateVerificationResult VerifyCodeSigningCertificate(X509Certificate2 certificate, IReadOnlyList<X509Certificate2> extraCertificates)
4849
{
4950
return VerifyCertificate(certificate, extraCertificates, applicationPolicy: new Oid(CodeSigningEku));
5051
}
5152

52-
public CertificateVerificationResult VerifyTimestampingCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates)
53+
public CertificateVerificationResult VerifyTimestampingCertificate(X509Certificate2 certificate, IReadOnlyList<X509Certificate2> extraCertificates)
5354
{
5455
return VerifyCertificate(certificate, extraCertificates, applicationPolicy: new Oid(TimeStampingEku));
5556
}
5657

57-
private CertificateVerificationResult VerifyCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates, Oid applicationPolicy)
58+
private CertificateVerificationResult VerifyCertificate(X509Certificate2 certificate, IReadOnlyList<X509Certificate2> extraCertificates, Oid applicationPolicy)
5859
{
5960
X509Chain chain = null;
6061

@@ -63,7 +64,7 @@ private CertificateVerificationResult VerifyCertificate(X509Certificate2 certifi
6364
chain = new X509Chain();
6465

6566
// Allow the chain to use whatever additional extra certificates were provided.
66-
chain.ChainPolicy.ExtraStore.AddRange(extraCertificates);
67+
chain.ChainPolicy.ExtraStore.AddRange(extraCertificates.ToArray());
6768

6869
if (applicationPolicy != null)
6970
{

0 commit comments

Comments
 (0)