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 . Linq ;
6+ using System . Security . Cryptography ;
7+ using System . Security . Cryptography . X509Certificates ;
8+ using Microsoft . Win32 . SafeHandles ;
9+ using NuGet . Services . Validation ;
10+
11+ namespace Validation . PackageSigning . ValidateCertificate
12+ {
13+ /// <summary>
14+ /// Performs online revocation verification for a <see cref="X509Certificate2"/>.
15+ /// </summary>
16+ /// <remarks>
17+ /// This depends on Windows' native CryptoApi and will only work on Windows. This
18+ /// verifier ignores NotTimeValid certificate statuses.
19+ /// </remarks>
20+ public class OnlineCertificateVerifier : ICertificateVerifier
21+ {
22+ /// <summary>
23+ /// RFC 5280 codeSigning attribute, https://tools.ietf.org/html/rfc5280#section-4.2.1.12
24+ /// </summary>
25+ private const string CodeSigningEku = "1.3.6.1.5.5.7.3.3" ;
26+
27+ /// <summary>
28+ /// RFC 3280 "id-kp-timeStamping" https://tools.ietf.org/html/rfc3280.html#section-4.2.1.13
29+ /// </summary>
30+ private const string TimeStampingEku = "1.3.6.1.5.5.7.3.8" ;
31+
32+ /// <summary>
33+ /// Chain status flags indicating that the revocation status could not be determined.
34+ /// </summary>
35+ private const X509ChainStatusFlags UnknownStatusFlags = X509ChainStatusFlags . RevocationStatusUnknown | X509ChainStatusFlags . OfflineRevocation ;
36+
37+ /// <summary>
38+ /// Certificate trust errors indicating that the certificate was not verified online.
39+ /// </summary>
40+ private const CertTrustErrorStatus OfflineErrorStatusFlags = CertTrustErrorStatus . CERT_TRUST_REVOCATION_STATUS_UNKNOWN | CertTrustErrorStatus . CERT_TRUST_IS_OFFLINE_REVOCATION ;
41+
42+ public CertificateVerificationResult VerifyCertificate ( X509Certificate2 certificate , X509Certificate2 [ ] extraCertificates )
43+ {
44+ return VerifyCertificate ( certificate , extraCertificates , applicationPolicy : null ) ;
45+ }
46+
47+ public CertificateVerificationResult VerifyCodeSigningCertificate ( X509Certificate2 certificate , X509Certificate2 [ ] extraCertificates )
48+ {
49+ return VerifyCertificate ( certificate , extraCertificates , applicationPolicy : new Oid ( CodeSigningEku ) ) ;
50+ }
51+
52+ public CertificateVerificationResult VerifyTimestampingCertificate ( X509Certificate2 certificate , X509Certificate2 [ ] extraCertificates )
53+ {
54+ return VerifyCertificate ( certificate , extraCertificates , applicationPolicy : new Oid ( TimeStampingEku ) ) ;
55+ }
56+
57+ private CertificateVerificationResult VerifyCertificate ( X509Certificate2 certificate , X509Certificate2 [ ] extraCertificates , Oid applicationPolicy )
58+ {
59+ X509Chain chain = null ;
60+
61+ try
62+ {
63+ chain = new X509Chain ( ) ;
64+
65+ // Allow the chain to use whatever additional extra certificates were provided.
66+ chain . ChainPolicy . ExtraStore . AddRange ( extraCertificates ) ;
67+
68+ if ( applicationPolicy != null )
69+ {
70+ chain . ChainPolicy . ApplicationPolicy . Add ( applicationPolicy ) ;
71+ }
72+
73+ chain . ChainPolicy . RevocationMode = X509RevocationMode . Online ;
74+ chain . ChainPolicy . RevocationFlag = X509RevocationFlag . ExcludeRoot ;
75+
76+ var resultBuilder = new CertificateVerificationResult . Builder ( ) ;
77+
78+ if ( chain . Build ( certificate ) )
79+ {
80+ resultBuilder . WithStatus ( EndCertificateStatus . Good ) ;
81+ resultBuilder . WithStatusFlags ( X509ChainStatusFlags . NoError ) ;
82+ }
83+ else
84+ {
85+ resultBuilder . WithStatus ( GetEndCertificateStatusFromInvalidChain ( chain ) ) ;
86+ resultBuilder . WithStatusFlags ( FlattenChainStatusFlags ( chain ) ) ;
87+ }
88+
89+ InspectChain ( chain , certificate , resultBuilder ) ;
90+
91+ return resultBuilder . Build ( ) ;
92+ }
93+ finally
94+ {
95+ if ( chain != null )
96+ {
97+ foreach ( var chainElement in chain . ChainElements )
98+ {
99+ chainElement . Certificate . Dispose ( ) ;
100+ }
101+
102+ chain . Dispose ( ) ;
103+ }
104+ }
105+ }
106+
107+ private EndCertificateStatus GetEndCertificateStatusFromInvalidChain ( X509Chain chain )
108+ {
109+ // There are multiple reasons why an end certificate may not have a status of EndCertificateStatus.Good:
110+ //
111+ // * The end certificate may be revoked or invalid.
112+ // * An issuing ancestor of the end certificate may be revoked or invalid.
113+ // * Any combination of the above cases.
114+ //
115+ // If the ONLY issue is that the end certificate is revoked, then package signatures created after the revocation will be
116+ // affected. In any other case where any certificate has a revoked or invalid status, the end certificate status will be
117+ // invalid.
118+ //
119+ // NOTE: This means that an end certificate that is revoked but has an ignored flag (like "NotTimeNested") will be
120+ // determined to be invalid here.
121+ if ( OnlyEndCertificateRevokedInInvalidChain ( chain ) )
122+ {
123+ return EndCertificateStatus . Revoked ;
124+ }
125+
126+ // If ANY status is anything other RevocationStatusUnknown, OfflineRevocation or NoError, the certificate is invalid and
127+ // dependent signatures should be invalidated.
128+ if ( chain . ChainStatus . Any ( s => ( s . Status & ~ UnknownStatusFlags ) != X509ChainStatusFlags . NoError ) )
129+ {
130+ return EndCertificateStatus . Invalid ;
131+ }
132+
133+ // All status flags are RevocationStatusUnknown, OfflineRevocation, or NoError. The certificate's verification should be
134+ // retried later.
135+ return EndCertificateStatus . Unknown ;
136+ }
137+
138+ private bool OnlyEndCertificateRevokedInInvalidChain ( X509Chain chain )
139+ {
140+ // Ensure that all of the chain statuses are the Revoked status flag.
141+ if ( chain . ChainStatus . Any ( s => ( s . Status & ~ X509ChainStatusFlags . Revoked ) != X509ChainStatusFlags . NoError ) )
142+ {
143+ return false ;
144+ }
145+
146+ // All chain statuses are Revoked status flags. Ensure that the end certificate has at least one of these Revoked statuses.
147+ if ( ! chain . ChainElements [ 0 ] . ChainElementStatus . Any ( ) )
148+ {
149+ return false ;
150+ }
151+
152+ // Ensure that all parent certificates have no errors.
153+ var parentElements = chain . ChainElements
154+ . Cast < X509ChainElement > ( )
155+ . Skip ( 1 ) ;
156+
157+ return ( parentElements . All ( e => e . ChainElementStatus . All ( s => s . Status == X509ChainStatusFlags . NoError ) ) ) ;
158+ }
159+
160+ private X509ChainStatusFlags FlattenChainStatusFlags ( X509Chain chain )
161+ {
162+ var result = X509ChainStatusFlags . NoError ;
163+
164+ foreach ( var chainStatus in chain . ChainStatus )
165+ {
166+ result |= chainStatus . Status ;
167+ }
168+
169+ return result ;
170+ }
171+
172+ private unsafe void InspectChain ( X509Chain chain , X509Certificate2 certificate , CertificateVerificationResult . Builder resultBuilder )
173+ {
174+ var addedRef = false ;
175+ var chainHandle = chain . SafeHandle ;
176+
177+ try
178+ {
179+ chainHandle . DangerousAddRef ( ref addedRef ) ;
180+
181+ CERT_REVOCATION_INFO * pRevocationInfo = GetEndCertificateRevocationInfoPointer ( chainHandle , certificate ) ;
182+
183+ if ( CertificateWasVerifiedOnline ( pRevocationInfo ) )
184+ {
185+ resultBuilder . WithRevocationTime ( GetRevocationTime ( pRevocationInfo ) ) ;
186+ resultBuilder . WithStatusUpdateTime ( GetStatusUpdateTime ( pRevocationInfo ) ) ;
187+ }
188+ }
189+ finally
190+ {
191+ if ( addedRef )
192+ {
193+ chainHandle . DangerousRelease ( ) ;
194+ }
195+ }
196+ }
197+
198+ private unsafe CERT_REVOCATION_INFO * GetEndCertificateRevocationInfoPointer ( SafeX509ChainHandle chainHandle , X509Certificate2 certificate )
199+ {
200+ CERT_CHAIN_CONTEXT * pCertChainContext = ( CERT_CHAIN_CONTEXT * ) ( chainHandle . DangerousGetHandle ( ) ) ;
201+
202+ if ( pCertChainContext == null )
203+ {
204+ throw new CertificateVerificationException ( $ "Certificate's { certificate . Thumbprint } CERT_CHAIN_CONTEXT* should never be null on Windows") ;
205+ }
206+
207+ if ( pCertChainContext ->cChain < 1 )
208+ {
209+ throw new CertificateVerificationException ( $ "Certificate's { certificate . Thumbprint } CERT_CHAIN_CONTEXT* should have at least one chain") ;
210+ }
211+
212+ if ( pCertChainContext ->rgpChain [ 0 ] ->cElement < 1 )
213+ {
214+ throw new CertificateVerificationException ( $ "Certificate's { certificate . Thumbprint } CERT_CHAIN_CONTEXT*'s first chain should have at least one element") ;
215+ }
216+
217+ CERT_SIMPLE_CHAIN * pCertSimpleChain = pCertChainContext ->rgpChain [ 0 ] ;
218+ CERT_CHAIN_ELEMENT * pChainElement = pCertSimpleChain ->rgpElement [ 0 ] ;
219+
220+ return pChainElement ->pRevocationInfo ;
221+ }
222+
223+ private unsafe bool CertificateWasVerifiedOnline ( CERT_REVOCATION_INFO * pRevocationInfo )
224+ {
225+ if ( pRevocationInfo == null || pRevocationInfo ->pCrlInfo == null )
226+ {
227+ return false ;
228+ }
229+
230+ return ( pRevocationInfo ->dwRevocationResult & OfflineErrorStatusFlags ) == 0 ;
231+ }
232+
233+ private unsafe DateTime ? GetRevocationTime ( CERT_REVOCATION_INFO * pRevocationInfo )
234+ {
235+ if ( pRevocationInfo ->dwRevocationResult == CertTrustErrorStatus . CERT_TRUST_NO_ERROR )
236+ {
237+ return null ;
238+ }
239+
240+ FILETIME revocationDate = pRevocationInfo ->pCrlInfo ->pCrlEntry ->RevocationDate ;
241+
242+ return revocationDate . ToDateTime ( ) . ToUniversalTime ( ) ;
243+ }
244+
245+ private unsafe DateTime ? GetStatusUpdateTime ( CERT_REVOCATION_INFO * pRevocationInfo )
246+ {
247+ CERT_REVOCATION_CRL_INFO * pCrlInfo = pRevocationInfo ->pCrlInfo ;
248+
249+ if ( pCrlInfo ->pDeltaCRLContext != null )
250+ {
251+ FILETIME statusUpdate = pCrlInfo ->pDeltaCRLContext ->pCrlInfo ->ThisUpdate ;
252+
253+ return statusUpdate . ToDateTime ( ) . ToUniversalTime ( ) ;
254+ }
255+ else if ( pCrlInfo ->pBaseCRLContext != null )
256+ {
257+ FILETIME statusUpdate = pCrlInfo ->pBaseCRLContext ->pCrlInfo ->ThisUpdate ;
258+
259+ return statusUpdate . ToDateTime ( ) . ToUniversalTime ( ) ;
260+ }
261+
262+ return null ;
263+ }
264+ }
265+ }
0 commit comments