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 . Threading ;
6+ using System . Threading . Tasks ;
7+ using Microsoft . Extensions . Logging ;
8+ using NuGet . Jobs . Validation . PackageSigning . Messages ;
9+ using NuGet . Jobs . Validation . PackageSigning . Storage ;
10+ using NuGet . Services . ServiceBus ;
11+ using NuGet . Services . Validation ;
12+
13+ namespace Validation . PackageSigning . ValidateCertificate
14+ {
15+ /// <summary>
16+ /// The handler for <see cref="CertificateValidationMessage"/>. Upon receiving a message,
17+ /// this will validate a <see cref="X509Certificate2"/> and perform online revocation checks.
18+ /// </summary>
19+ public sealed class CertificateValidationMessageHandler : IMessageHandler < CertificateValidationMessage >
20+ {
21+ private readonly ICertificateStore _certificateStore ;
22+ private readonly ICertificateValidationService _certificateValidationService ;
23+ private readonly ILogger < CertificateValidationMessageHandler > _logger ;
24+
25+ private readonly int _maximumValidationFailures ;
26+
27+ public CertificateValidationMessageHandler (
28+ ICertificateStore certificateStore ,
29+ ICertificateValidationService certificateValidationService ,
30+ ILogger < CertificateValidationMessageHandler > logger ,
31+ int maximumValidationFailures = CertificateValidationService . DefaultMaximumValidationFailures )
32+ {
33+ _certificateStore = certificateStore ?? throw new ArgumentNullException ( nameof ( certificateStore ) ) ;
34+ _certificateValidationService = certificateValidationService ?? throw new ArgumentNullException ( nameof ( certificateValidationService ) ) ;
35+ _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
36+
37+ _maximumValidationFailures = maximumValidationFailures ;
38+ }
39+
40+ /// <summary>
41+ /// Perform the certificate validation request, including online revocation checks.
42+ /// </summary>
43+ /// <param name="message">The message requesting the certificate validation.</param>
44+ /// <returns>Whether the validation completed. If false, the validation should be retried later.</returns>
45+ public async Task < bool > HandleAsync ( CertificateValidationMessage message )
46+ {
47+ var validation = await _certificateValidationService . FindCertificateValidationAsync ( message ) ;
48+
49+ if ( validation == null )
50+ {
51+ _logger . LogInformation (
52+ "Could not find a certificate validation entity, failing (certificate: {CertificateKey} validation: {ValidationId})" ,
53+ message . CertificateKey ,
54+ message . ValidationId ) ;
55+
56+ return false ;
57+ }
58+
59+ if ( validation . Status != null )
60+ {
61+ // A certificate validation should be queued with a Status of null, and once the certificate validation
62+ // completes, the Status should be updated to a non-null value. Hence, the Status here SHOULD be null.
63+ // A non-null Status may indicate message duplication.
64+ _logger . LogWarning (
65+ "Invalid certificate validation entity's status, dropping message (certificate: {CertificateThumbprint} validation: {ValidationId})" ,
66+ validation . EndCertificate . Thumbprint ,
67+ validation . ValidationId ) ;
68+
69+ return true ;
70+ }
71+
72+ if ( validation . EndCertificate . Status == EndCertificateStatus . Revoked )
73+ {
74+ if ( message . RevalidateRevokedCertificate )
75+ {
76+ _logger . LogWarning (
77+ "Revalidating certificate that is known to be revoked " +
78+ "(certificate: {CertificateThumbprint} validation: {ValidationId})" ,
79+ validation . EndCertificate . Thumbprint ,
80+ validation . ValidationId ) ;
81+ }
82+ else
83+ {
84+ // Do NOT revalidate a certificate that is known to be revoked unless explicitly told to!
85+ // Certificate Authorities are not required to keep a certificate's revocation information
86+ // forever, therefore, revoked certificates should only be revalidated in special cases.
87+ _logger . LogError (
88+ "Certificate known to be revoked MUST be validated with the " +
89+ $ "{ nameof ( CertificateValidationMessage . RevalidateRevokedCertificate ) } flag enabled " +
90+ "(certificate: {CertificateThumbprint} validation: {ValidationId})" ,
91+ validation . EndCertificate . Thumbprint ,
92+ validation . ValidationId ) ;
93+
94+ return true ;
95+ }
96+ }
97+
98+ // Download and verify the certificate.
99+ var certificate = await _certificateStore . LoadAsync ( validation . EndCertificate . Thumbprint , CancellationToken . None ) ;
100+ var result = await _certificateValidationService . VerifyAsync ( certificate ) ;
101+
102+ // Save the result. This may alert if packages are invalidated.
103+ if ( ! await _certificateValidationService . TrySaveResultAsync ( validation , result ) )
104+ {
105+ _logger . LogWarning (
106+ "Failed to save certificate validation result " +
107+ "(certificate: {CertificateThumbprint} validation: {ValidationId}), " +
108+ "failing validation" ,
109+ validation . EndCertificate . Thumbprint ,
110+ validation . ValidationId ) ;
111+
112+ return false ;
113+ }
114+
115+ return HasValidationCompleted ( validation , result ) ;
116+ }
117+
118+ private bool HasValidationCompleted ( EndCertificateValidation validation , CertificateVerificationResult result )
119+ {
120+ // The validation is complete if the certificate was determined to be "Good", "Invalid", or "Revoked".
121+ if ( result . Status == EndCertificateStatus . Good
122+ || result . Status == EndCertificateStatus . Invalid
123+ || result . Status == EndCertificateStatus . Revoked )
124+ {
125+ return true ;
126+ }
127+ else if ( result . Status == EndCertificateStatus . Unknown )
128+ {
129+ // Certificates whose status failed to be determined will have an "Unknown"
130+ // status. These certificates should be retried until "_maximumValidationFailures"
131+ // is reached.
132+ if ( validation . EndCertificate . ValidationFailures >= _maximumValidationFailures )
133+ {
134+ _logger . LogWarning (
135+ "Certificate {CertificateThumbprint} has reached maximum of {MaximumValidationFailures} failed validation attempts" ,
136+ validation . EndCertificate . Thumbprint ,
137+ _maximumValidationFailures ) ;
138+
139+ return true ;
140+ }
141+ else
142+ {
143+ _logger . LogWarning (
144+ "Could not validate certificate {CertificateThumbprint}, {RetriesLeft} retries left" ,
145+ validation . EndCertificate . Thumbprint ,
146+ _maximumValidationFailures - validation . EndCertificate . ValidationFailures ) ;
147+
148+ return false ;
149+ }
150+ }
151+
152+ _logger . LogError (
153+ $ "Unknown { nameof ( EndCertificateStatus ) } value: {{CertificateStatus}}, throwing to retry",
154+ result . Status ) ;
155+
156+ throw new InvalidOperationException ( $ "Unknown { nameof ( EndCertificateStatus ) } value: { result . Status } ") ;
157+ }
158+ }
159+ }
0 commit comments