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

Commit 500e467

Browse files
authored
[Package Signing] Online Revocation Checking (#309)
Adds the groundwork to perform online revocation checking of certificates. This will be used by nuget.org's validation pipeline to ensure that packages are signed with certificates that are valid. This code will be run on Windows Server only on .NET Framework v4.6.1+.
1 parent a040d6d commit 500e467

13 files changed

Lines changed: 708 additions & 10 deletions

LICENSE.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
Copyright (c) .NET Foundation. All rights reserved.
1+
Copyright (c) .NET Foundation and Contributors.
2+
3+
All rights reserved.
24

35
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
46
these files except in compliance with the License. You may obtain a copy of the

src/Validation.PackageSigning.ValidateCertificate/CertificateValidationMessageHandler.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ public async Task<bool> HandleAsync(CertificateValidationMessage message)
9696
}
9797

9898
// 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
99101
var certificate = await _certificateStore.LoadAsync(validation.EndCertificate.Thumbprint, CancellationToken.None);
100102
var result = await _certificateValidationService.VerifyAsync(certificate);
101103

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
6+
namespace Validation.PackageSigning.ValidateCertificate
7+
{
8+
public class CertificateVerificationException : Exception
9+
{
10+
/// <summary>
11+
/// Exception thrown by unexpected failures to validate a certificate.
12+
/// </summary>
13+
/// <param name="message">The message describing the failure.</param>
14+
public CertificateVerificationException(string message)
15+
: base(message)
16+
{
17+
}
18+
}
19+
}

src/Validation.PackageSigning.ValidateCertificate/ICertificateValidationService.cs

Lines changed: 0 additions & 1 deletion
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;
54
using System.Security.Cryptography.X509Certificates;
65
using System.Threading.Tasks;
76
using NuGet.Jobs.Validation.PackageSigning.Messages;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.Security.Cryptography.X509Certificates;
6+
7+
namespace Validation.PackageSigning.ValidateCertificate
8+
{
9+
/// <summary>
10+
/// Verifies <see cref="X509Certificate2"/>.
11+
/// </summary>
12+
public interface ICertificateVerifier
13+
{
14+
/// <summary>
15+
/// Determine the status of a <see cref="X509Certificate2"/>.
16+
/// </summary>
17+
/// <param name="certificate">The certificate to verify.</param>
18+
/// <param name="extraCertificates">A collection of certificates that may be used to build the certificate chain.</param>
19+
/// <returns>The result of the verification.</returns>
20+
[Obsolete("This will be removed when integration tests are created")]
21+
CertificateVerificationResult VerifyCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates);
22+
23+
/// <summary>
24+
/// Determine the status of a code signing <see cref="X509Certificate2"/>.
25+
/// </summary>
26+
/// <param name="certificate">The certificate to verify.</param>
27+
/// <param name="extraCertificates">A collection of certificates that may be used to build the certificate chain.</param>
28+
/// <returns>The result of the verification.</returns>
29+
CertificateVerificationResult VerifyCodeSigningCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates);
30+
31+
/// <summary>
32+
/// Determine the status of a timestamping <see cref="X509Certificate2"/>.
33+
/// </summary>
34+
/// <param name="certificate">The certificate to verify.</param>
35+
/// <param name="extraCertificates">A collection of certificates that may be used to build the certificate chain.</param>
36+
/// <returns>The result of the verification.</returns>
37+
CertificateVerificationResult VerifyTimestampingCertificate(X509Certificate2 certificate, X509Certificate2[] extraCertificates);
38+
}
39+
}

src/Validation.PackageSigning.ValidateCertificate/Job.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics;
76
using System.Threading.Tasks;
87
using Autofac;
98
using Autofac.Extensions.DependencyInjection;
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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

Comments
 (0)