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

Commit e81fa28

Browse files
committed
Implement ICertificateStore
Complete NuGet/Engineering#932
1 parent b5c7d3f commit e81fa28

23 files changed

Lines changed: 591 additions & 68 deletions

File tree

NuGet.Jobs.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.PackageSigning.E
103103
EndProject
104104
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.PackageSigning.ExtractAndValidateSignature.Tests", "tests\Validation.PackageSigning.ExtractAndValidateSignature.Tests\Validation.PackageSigning.ExtractAndValidateSignature.Tests.csproj", "{26435822-8938-48C9-96FD-0DCCF8F7CE00}"
105105
EndProject
106+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.PackageSigning.Core.Tests", "tests\Validation.PackageSigning.Core.Tests\Validation.PackageSigning.Core.Tests.csproj", "{B4B7564A-965B-447B-927F-6749E2C08880}"
107+
EndProject
106108
Global
107109
GlobalSection(SolutionConfigurationPlatforms) = preSolution
108110
Debug|Any CPU = Debug|Any CPU
@@ -259,6 +261,10 @@ Global
259261
{26435822-8938-48C9-96FD-0DCCF8F7CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU
260262
{26435822-8938-48C9-96FD-0DCCF8F7CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
261263
{26435822-8938-48C9-96FD-0DCCF8F7CE00}.Release|Any CPU.Build.0 = Release|Any CPU
264+
{B4B7564A-965B-447B-927F-6749E2C08880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
265+
{B4B7564A-965B-447B-927F-6749E2C08880}.Debug|Any CPU.Build.0 = Debug|Any CPU
266+
{B4B7564A-965B-447B-927F-6749E2C08880}.Release|Any CPU.ActiveCfg = Release|Any CPU
267+
{B4B7564A-965B-447B-927F-6749E2C08880}.Release|Any CPU.Build.0 = Release|Any CPU
262268
EndGlobalSection
263269
GlobalSection(SolutionProperties) = preSolution
264270
HideSolutionNode = FALSE
@@ -300,6 +306,7 @@ Global
300306
{91C060DA-736F-4DA9-A57F-CB3AC0E6CB10} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02}
301307
{DD043977-6BCD-475A-BEE2-8C34309EC622} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02}
302308
{26435822-8938-48C9-96FD-0DCCF8F7CE00} = {6A776396-02B1-475D-A104-26940ADB04AB}
309+
{B4B7564A-965B-447B-927F-6749E2C08880} = {6A776396-02B1-475D-A104-26940ADB04AB}
303310
EndGlobalSection
304311
GlobalSection(ExtensibilityGlobals) = postSolution
305312
SolutionGuid = {284A7AC3-FB43-4F1F-9C9C-2AF0E1F46C2B}

src/NuGet.Services.Validation.Orchestrator/IValidationStorageService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public interface IValidationStorageService
3535
/// or <see cref="GetValidationSetAsync(Guid)"/> calls.</param>
3636
/// <param name="validationResult">Validation result. Its status cannot be <see cref="ValidationStatus.NotStarted"/></param>
3737
/// <returns>Task object tracking the async operation status.</returns>
38-
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="startedStatus"/> is <see cref="ValidationStatus.NotStarted"/></exception>
38+
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="validationResult"/> has status <see cref="ValidationStatus.NotStarted"/></exception>
3939
Task MarkValidationStartedAsync(PackageValidation packageValidation, IValidationResult validationResult);
4040

4141
/// <summary>

src/Validation.Common/app.config

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,74 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<configuration>
33
<runtime>
44
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
55
<dependentAssembly>
6-
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
7-
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
6+
<assemblyIdentity name="NuGet.Services.Logging" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
7+
<bindingRedirect oldVersion="0.0.0.0-2.7.0.0" newVersion="2.7.0.0"/>
88
</dependentAssembly>
99
<dependentAssembly>
10-
<assemblyIdentity name="Microsoft.Data.Edm" publicKeyToken="31bf3856ad364e35" culture="neutral" />
11-
<bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
10+
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
11+
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
1212
</dependentAssembly>
1313
<dependentAssembly>
14-
<assemblyIdentity name="Microsoft.Data.OData" publicKeyToken="31bf3856ad364e35" culture="neutral" />
15-
<bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
14+
<assemblyIdentity name="Microsoft.Data.Edm" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
15+
<bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0"/>
1616
</dependentAssembly>
1717
<dependentAssembly>
18-
<assemblyIdentity name="Microsoft.Data.Services.Client" publicKeyToken="31bf3856ad364e35" culture="neutral" />
19-
<bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
18+
<assemblyIdentity name="Microsoft.Data.OData" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
19+
<bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0"/>
2020
</dependentAssembly>
2121
<dependentAssembly>
22-
<assemblyIdentity name="Microsoft.Azure.KeyVault" publicKeyToken="31bf3856ad364e35" culture="neutral" />
23-
<bindingRedirect oldVersion="0.0.0.0-1.0.0.0" newVersion="1.0.0.0" />
22+
<assemblyIdentity name="Microsoft.Data.Services.Client" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
23+
<bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0"/>
2424
</dependentAssembly>
2525
<dependentAssembly>
26-
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory.Platform" publicKeyToken="31bf3856ad364e35" culture="neutral" />
27-
<bindingRedirect oldVersion="0.0.0.0-3.13.5.907" newVersion="3.13.5.907" />
26+
<assemblyIdentity name="Microsoft.Azure.KeyVault" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
27+
<bindingRedirect oldVersion="0.0.0.0-1.0.0.0" newVersion="1.0.0.0"/>
2828
</dependentAssembly>
2929
<dependentAssembly>
30-
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory" publicKeyToken="31bf3856ad364e35" culture="neutral" />
31-
<bindingRedirect oldVersion="0.0.0.0-3.13.5.907" newVersion="3.13.5.907" />
30+
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory.Platform" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
31+
<bindingRedirect oldVersion="0.0.0.0-3.13.5.907" newVersion="3.13.5.907"/>
3232
</dependentAssembly>
3333
<dependentAssembly>
34-
<assemblyIdentity name="Microsoft.ApplicationInsights" publicKeyToken="31bf3856ad364e35" culture="neutral" />
35-
<bindingRedirect oldVersion="0.0.0.0-2.2.0.0" newVersion="2.2.0.0" />
34+
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
35+
<bindingRedirect oldVersion="0.0.0.0-3.13.5.907" newVersion="3.13.5.907"/>
3636
</dependentAssembly>
3737
<dependentAssembly>
38-
<assemblyIdentity name="NuGet.Services.KeyVault" publicKeyToken="31bf3856ad364e35" culture="neutral" />
39-
<bindingRedirect oldVersion="0.0.0.0-2.7.0.0" newVersion="2.7.0.0" />
38+
<assemblyIdentity name="Microsoft.ApplicationInsights" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
39+
<bindingRedirect oldVersion="0.0.0.0-2.2.0.0" newVersion="2.2.0.0"/>
4040
</dependentAssembly>
4141
<dependentAssembly>
42-
<assemblyIdentity name="Microsoft.Extensions.Configuration.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
43-
<bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0" />
42+
<assemblyIdentity name="NuGet.Services.KeyVault" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
43+
<bindingRedirect oldVersion="0.0.0.0-2.7.0.0" newVersion="2.7.0.0"/>
4444
</dependentAssembly>
4545
<dependentAssembly>
46-
<assemblyIdentity name="Microsoft.Extensions.FileProviders.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
47-
<bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
46+
<assemblyIdentity name="Microsoft.Extensions.Configuration.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral"/>
47+
<bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0"/>
4848
</dependentAssembly>
4949
<dependentAssembly>
50-
<assemblyIdentity name="Microsoft.Extensions.Configuration.FileExtensions" publicKeyToken="adb9793829ddae60" culture="neutral" />
51-
<bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0" />
50+
<assemblyIdentity name="Microsoft.Extensions.FileProviders.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral"/>
51+
<bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0"/>
5252
</dependentAssembly>
5353
<dependentAssembly>
54-
<assemblyIdentity name="Microsoft.Extensions.FileProviders.Physical" publicKeyToken="adb9793829ddae60" culture="neutral" />
55-
<bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
54+
<assemblyIdentity name="Microsoft.Extensions.Configuration.FileExtensions" publicKeyToken="adb9793829ddae60" culture="neutral"/>
55+
<bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0"/>
5656
</dependentAssembly>
5757
<dependentAssembly>
58-
<assemblyIdentity name="Microsoft.Extensions.Configuration" publicKeyToken="adb9793829ddae60" culture="neutral" />
59-
<bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0" />
58+
<assemblyIdentity name="Microsoft.Extensions.FileProviders.Physical" publicKeyToken="adb9793829ddae60" culture="neutral"/>
59+
<bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0"/>
6060
</dependentAssembly>
6161
<dependentAssembly>
62-
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
63-
<bindingRedirect oldVersion="0.0.0.0-1.0.0.0" newVersion="1.0.0.0" />
62+
<assemblyIdentity name="Microsoft.Extensions.Configuration" publicKeyToken="adb9793829ddae60" culture="neutral"/>
63+
<bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0"/>
64+
</dependentAssembly>
65+
<dependentAssembly>
66+
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral"/>
67+
<bindingRedirect oldVersion="0.0.0.0-1.0.0.0" newVersion="1.0.0.0"/>
6468
</dependentAssembly>
6569
</assemblyBinding>
6670
</runtime>
67-
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /></startup></configuration>
71+
<startup>
72+
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
73+
</startup>
74+
</configuration>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
namespace NuGet.Jobs.Validation.PackageSigning.Configuration
5+
{
6+
public class CertificateStoreConfiguration
7+
{
8+
public string DataStorageAccount { get; set; }
9+
public string ContainerName { get; set; }
10+
}
11+
}

src/Validation.PackageSigning.Core/Error.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public static class Error
99
{
1010
public static EventId ValidatorStateServiceFailedToAddStatus = new EventId(1000, "Failed to add validator's status");
1111
public static EventId ValidatorStateServiceFailedToUpdateStatus = new EventId(1001, "Failed to update validator's status");
12+
public static EventId LoadedCertificateThumbprintDoesNotMatch = new EventId(1002, "Certificate thumbprint mismatch");
13+
public static EventId LoadCertificateFromStorageFailed = new EventId(1003, "Certificate loading from storage failed");
1214

1315
public static EventId ValidateSignatureFailedToDownloadPackageStatus = new EventId(1100, "Failed to download package");
1416
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
namespace System.Security.Cryptography.X509Certificates
5+
{
6+
public static class X509Certificate2Extensions
7+
{
8+
public static string ComputeSHA256Thumbprint(this X509Certificate2 certificate)
9+
{
10+
if (certificate == null)
11+
{
12+
throw new ArgumentNullException(nameof(certificate));
13+
}
14+
15+
using (var sha256 = SHA256.Create())
16+
{
17+
var digestBytes = sha256.ComputeHash(certificate.RawData);
18+
return BitConverter
19+
.ToString(digestBytes)
20+
.Replace("-", string.Empty)
21+
.ToLowerInvariant();
22+
}
23+
}
24+
}
25+
}

src/Validation.PackageSigning.Core/Storage/CertificateStore.cs

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,116 @@
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.IO;
56
using System.Security.Cryptography.X509Certificates;
7+
using System.Threading;
68
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
using NuGet.Services.Storage;
711

812
namespace NuGet.Jobs.Validation.PackageSigning.Storage
913
{
1014
public class CertificateStore : ICertificateStore
1115
{
12-
public Task<bool> Exists(string thumbprint)
16+
private const string _containerSubDirectory = "sha256";
17+
private const string _fileExtension = ".cer";
18+
19+
private readonly ILogger<CertificateStore> _logger;
20+
private readonly IStorage _storage;
21+
22+
public CertificateStore(
23+
IStorage storage,
24+
ILogger<CertificateStore> logger)
25+
{
26+
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
27+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
28+
}
29+
30+
public Task<bool> ExistsAsync(string sha256Thumbprint, CancellationToken cancellationToken)
1331
{
14-
throw new NotImplementedException();
32+
if (sha256Thumbprint == null)
33+
{
34+
throw new ArgumentNullException(nameof(sha256Thumbprint));
35+
}
36+
37+
return _storage.ExistsAsync(GetBlobFileName(sha256Thumbprint), cancellationToken);
1538
}
1639

17-
public Task<X509Certificate2> Load(string thumbprint)
40+
public async Task<X509Certificate2> LoadAsync(string sha256Thumbprint, CancellationToken cancellationToken)
1841
{
19-
// TODO: verify the certificate's thumbprint each time the certificate is downloaded from blob storage
42+
if (sha256Thumbprint == null)
43+
{
44+
throw new ArgumentNullException(nameof(sha256Thumbprint));
45+
}
46+
47+
var uri = _storage.ResolveUri(GetBlobFileName(sha256Thumbprint));
48+
49+
_logger.LogInformation("Loading certificate with SHA-256 thumbprint {Thumbprint} from URI {BlobUri}", sha256Thumbprint, uri);
50+
51+
var storageContent = await _storage.Load(uri, CancellationToken.None);
52+
if (storageContent == null)
53+
{
54+
_logger.LogError(
55+
Error.LoadCertificateFromStorageFailed,
56+
"The certificate with SHA-256 thumbprint {Thumbprint} could not be loaded from storage URI {BlobUri}.",
57+
sha256Thumbprint,
58+
uri);
59+
60+
throw new InvalidOperationException($"Failed to load certificate with SHA-256 thumbprint {sha256Thumbprint} from URI {uri}");
61+
}
62+
63+
byte[] rawData;
64+
using (var stream = storageContent.GetContentStream())
65+
{
66+
using (var buffer = new MemoryStream())
67+
{
68+
await stream.CopyToAsync(buffer);
69+
rawData = buffer.ToArray();
70+
}
71+
}
72+
73+
var certificate = new X509Certificate2(rawData);
74+
var certificateSha256ComputedThumbprint = certificate.ComputeSHA256Thumbprint();
75+
76+
// Verify the certificate's thumbprint each time the certificate is downloaded from blob storage
77+
if (!string.Equals(certificateSha256ComputedThumbprint, sha256Thumbprint, StringComparison.Ordinal))
78+
{
79+
_logger.LogError(
80+
Error.LoadedCertificateThumbprintDoesNotMatch,
81+
"The loaded certificate did not match the expected SHA-256 thumbprint {ExpectedThumbprint} (actual SHA-256 thumbprint: {ActualThumbprint}).",
82+
sha256Thumbprint,
83+
certificateSha256ComputedThumbprint);
84+
85+
throw new InvalidOperationException($"The loaded certificate did not match the expected SHA-256 thumbprint {sha256Thumbprint} (actual SHA-256 thumbprint: {certificateSha256ComputedThumbprint}).");
86+
}
87+
88+
_logger.LogInformation("Loaded certificate with SHA-256 thumbprint {Thumbprint} from URI {blobUri}", sha256Thumbprint, uri);
89+
90+
return certificate;
91+
}
92+
93+
public Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
94+
{
95+
if (certificate == null)
96+
{
97+
throw new ArgumentNullException(nameof(certificate));
98+
}
99+
100+
var sha256Thumbprint = certificate.ComputeSHA256Thumbprint();
101+
var uri = _storage.ResolveUri(GetBlobFileName(sha256Thumbprint));
102+
103+
_logger.LogInformation("Saving certificate with SHA-256 thumbprint {Thumbprint} to URI {BlobUri}", sha256Thumbprint, uri);
20104

21-
throw new NotImplementedException();
105+
return _storage.Save(
106+
uri,
107+
new StreamStorageContent(new MemoryStream(certificate.RawData)),
108+
overwrite: false,
109+
cancellationToken: cancellationToken);
22110
}
23111

24-
public Task Save(X509Certificate2 certificate)
112+
private static string GetBlobFileName(string sha256Thumbprint)
25113
{
26-
throw new NotImplementedException();
114+
return $"{_containerSubDirectory}/{sha256Thumbprint}{_fileExtension}".ToLowerInvariant();
27115
}
28116
}
29117
}

src/Validation.PackageSigning.Core/Storage/ICertificateStore.cs

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

44
using System.Security.Cryptography.X509Certificates;
5+
using System.Threading;
56
using System.Threading.Tasks;
67

78
namespace NuGet.Jobs.Validation.PackageSigning.Storage
89
{
910
/// <summary>
10-
/// The class used to <see cref="X509Certificate2"/> store and retrieve certificates.
11+
/// The interface used to <see cref="X509Certificate2"/> store and retrieve certificates.
1112
/// </summary>
1213
public interface ICertificateStore
1314
{
1415
/// <summary>
1516
/// Check if the store contains the certificate.
1617
/// </summary>
1718
/// <param name="thumbprint">The certificate's thumbprint.</param>
19+
/// <param name="cancellationToken">The cancellation token.</param>
1820
/// <returns>Whether the store contains a certificate that has the given thumbprint.</returns>
19-
Task<bool> Exists(string thumbprint);
21+
Task<bool> ExistsAsync(string thumbprint, CancellationToken cancellationToken);
2022

2123
/// <summary>
2224
/// Load the certificate into memory.
2325
/// </summary>
2426
/// <param name="thumbprint">The certificate's thumbprint.</param>
27+
/// <param name="cancellationToken">The cancellation token.</param>
2528
/// <returns>A certificate whose thumbprint is the given thumbprint.</returns>
26-
Task<X509Certificate2> Load(string thumbprint);
29+
Task<X509Certificate2> LoadAsync(string thumbprint, CancellationToken cancellationToken);
2730

2831
/// <summary>
29-
/// Save the certificate to the store.
32+
/// Save the certificate to the store. This method fails if the certificate already exists.
3033
/// </summary>
3134
/// <param name="certificate">The certificate to save to the store.</param>
35+
/// <param name="cancellationToken">The cancellation token.</param>
3236
/// <returns>A task that completes when the certificate has been saved.</returns>
33-
Task Save(X509Certificate2 certificate);
37+
Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken);
3438
}
3539
}

0 commit comments

Comments
 (0)