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

Commit 93d7c7a

Browse files
committed
For valid signed packages, extract certificates and write them to the certificate store (#295)
Progress on NuGet/Engineering#785
1 parent e81fa28 commit 93d7c7a

16 files changed

Lines changed: 570 additions & 5 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
5+
namespace System.Security.Cryptography
6+
{
7+
public static class CryptographicAttributeObjectCollectionExtensions
8+
{
9+
/// <summary>
10+
/// Returns the first attribute if the Oid is found.
11+
/// Returns null if the attribute is not found.
12+
/// </summary>
13+
public static CryptographicAttributeObject FirstOrDefault(this CryptographicAttributeObjectCollection attributes, string oid)
14+
{
15+
if (oid == null)
16+
{
17+
throw new ArgumentNullException(nameof(oid));
18+
}
19+
20+
foreach (var attribute in attributes)
21+
{
22+
if (StringComparer.Ordinal.Equals(oid, attribute.Oid.Value))
23+
{
24+
return attribute;
25+
}
26+
}
27+
28+
return null;
29+
}
30+
}
31+
}

src/Validation.PackageSigning.Core/Validation.PackageSigning.Core.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Reference Include="System.Core" />
3636
<Reference Include="Microsoft.CSharp" />
3737
<Reference Include="System.Data" />
38+
<Reference Include="System.Security" />
3839
</ItemGroup>
3940
<ItemGroup>
4041
<PackageReference Include="NuGet.Services.ServiceBus">
@@ -54,6 +55,7 @@
5455
<Compile Include="Configuration\CertificateStoreConfiguration.cs" />
5556
<Compile Include="Error.cs" />
5657
<Compile Include="ExceptionExtensions.cs" />
58+
<Compile Include="Extensions\CryptographicAttributeObjectCollectionExtensions.cs" />
5759
<Compile Include="Extensions\X509Certificate2Extensions.cs" />
5860
<Compile Include="Storage\AddStatusResult.cs" />
5961
<Compile Include="Storage\CertificateStore.cs" />
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.Collections.Generic;
6+
using NuGet.Common;
7+
8+
namespace NuGet.Jobs.Validation.PackageSigning.ExtractAndValidateSignature
9+
{
10+
/// <summary>
11+
/// Represents a hash digest and the hash algorithm that produced it. This allows easy set comparisons of many
12+
/// hashes with varying hash algorithms.
13+
/// </summary>
14+
public class Hash : IEquatable<Hash>
15+
{
16+
public Hash(HashAlgorithmName algorithmName, byte[] digest)
17+
{
18+
if (digest == null)
19+
{
20+
throw new ArgumentNullException(nameof(digest));
21+
}
22+
23+
AlgorithmName = algorithmName;
24+
HexDigest = BitConverter
25+
.ToString(digest)
26+
.Replace("-", string.Empty)
27+
.ToLowerInvariant();
28+
}
29+
30+
public HashAlgorithmName AlgorithmName { get; }
31+
public string HexDigest { get; }
32+
33+
public override bool Equals(object obj)
34+
{
35+
return Equals(obj as Hash);
36+
}
37+
38+
public bool Equals(Hash other)
39+
{
40+
if (other == null)
41+
{
42+
return false;
43+
}
44+
45+
if (ReferenceEquals(this, other))
46+
{
47+
return true;
48+
}
49+
50+
return AlgorithmName == other.AlgorithmName
51+
&& HexDigest == other.HexDigest;
52+
}
53+
54+
/// <summary>
55+
/// This method is auto-generated using Visual Studio.
56+
/// </summary>
57+
public override int GetHashCode()
58+
{
59+
unchecked
60+
{
61+
var hashCode = 696079939;
62+
hashCode = hashCode * -1521134295 + AlgorithmName.GetHashCode();
63+
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(HexDigest);
64+
return hashCode;
65+
}
66+
}
67+
}
68+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.Threading;
5+
using System.Threading.Tasks;
6+
using NuGet.Packaging.Signing;
7+
8+
namespace NuGet.Jobs.Validation.PackageSigning.ExtractAndValidateSignature
9+
{
10+
/// <summary>
11+
/// After a package's signature is validated, this interface extracts components of the signature and persists them
12+
/// to be used by downstream systems.
13+
/// </summary>
14+
public interface ISignaturePartsExtractor
15+
{
16+
/// <summary>
17+
/// Extracts and persists artifacts from the provided signed package.
18+
/// </summary>
19+
/// <exception cref="ArgumentException">Thrown if the provided package is not signed.</exception>
20+
/// <param name="signedPackageReader">The reader of the signed package.</param>
21+
/// <param name="token">The cancellation token.</param>
22+
Task ExtractAsync(ISignedPackageReader signedPackageReader, CancellationToken token);
23+
}
24+
}

src/Validation.PackageSigning.ExtractAndValidateSignature/Job.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ private void ConfigureJobServices(IServiceCollection services, IConfigurationRoo
184184
services.AddTransient<IMessageHandler<SignatureValidationMessage>, SignatureValidationMessageHandler>();
185185
services.AddTransient<IPackageSigningStateService, PackageSigningStateService>();
186186
services.AddTransient<ISignatureValidator, SignatureValidator>();
187+
services.AddTransient<ISignaturePartsExtractor, SignaturePartsExtractor>();
187188

188189
services.AddSingleton(p =>
189190
{
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.Collections.Generic;
6+
using System.Linq;
7+
using System.Security.Cryptography;
8+
using System.Security.Cryptography.Pkcs;
9+
using System.Security.Cryptography.X509Certificates;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using NuGet.Common;
13+
using NuGet.Jobs.Validation.PackageSigning.Storage;
14+
using NuGet.Packaging.Signing;
15+
16+
namespace NuGet.Jobs.Validation.PackageSigning.ExtractAndValidateSignature
17+
{
18+
public class SignaturePartsExtractor : ISignaturePartsExtractor
19+
{
20+
private readonly ICertificateStore _certificateStore;
21+
22+
public SignaturePartsExtractor(ICertificateStore certificateStore)
23+
{
24+
_certificateStore = certificateStore ?? throw new ArgumentNullException(nameof(certificateStore));
25+
}
26+
27+
public async Task ExtractAsync(ISignedPackageReader signedPackageReader, CancellationToken token)
28+
{
29+
if (!await signedPackageReader.IsSignedAsync(token))
30+
{
31+
throw new ArgumentException("The provided package reader must refer to a signed package.", nameof(signedPackageReader));
32+
}
33+
34+
var signatures = await signedPackageReader.GetSignaturesAsync(token);
35+
36+
foreach (var signature in signatures)
37+
{
38+
foreach (var certificate in GetCertificates(signature.SignedCms))
39+
{
40+
await SaveCertificateAsync(certificate, token);
41+
}
42+
43+
foreach (var timestamp in signature.Timestamps)
44+
{
45+
// TODO: use the signing-certificate-v2 attribute to prune.
46+
foreach (var certificate in timestamp.SignedCms.Certificates)
47+
{
48+
await SaveCertificateAsync(certificate, token);
49+
}
50+
}
51+
}
52+
}
53+
54+
private IEnumerable<X509Certificate2> GetCertificates(SignedCms signedCms)
55+
{
56+
// Use the signing-certificate-v2 attribute to prune the list of certificates.
57+
var signingCertificateV2Attribute = signedCms
58+
.SignerInfos[0]
59+
.SignedAttributes
60+
.FirstOrDefault(Oids.SigningCertificateV2);
61+
62+
if (signingCertificateV2Attribute == null)
63+
{
64+
throw new ArgumentException(
65+
$"The first element of {nameof(SignedCms.SignerInfos)} {nameof(SignedCms)} must have a signing certificate attribute.",
66+
nameof(signedCms));
67+
}
68+
69+
var signingCertificateHashes = new HashSet<Hash>(AttributeUtility
70+
.GetESSCertIDv2Entries(signingCertificateV2Attribute)
71+
.Select(pair => new Hash(pair.Key, pair.Value)));
72+
73+
// Try all of the candidate hash algorithms against the set of certificates found in the SignedCMS.
74+
var algorithmsToTry = signingCertificateHashes
75+
.Select(x => x.AlgorithmName)
76+
.Distinct();
77+
foreach (var algorithm in algorithmsToTry)
78+
{
79+
foreach (var certificate in signedCms.Certificates)
80+
{
81+
var digest = CryptoHashUtility.ComputeHash(algorithm, certificate.RawData);
82+
var hash = new Hash(algorithm, digest);
83+
84+
if (signingCertificateHashes.Contains(hash))
85+
{
86+
yield return certificate;
87+
}
88+
}
89+
}
90+
}
91+
92+
private async Task SaveCertificateAsync(X509Certificate2 certificate, CancellationToken token)
93+
{
94+
var thumbprint = certificate.ComputeSHA256Thumbprint();
95+
96+
if (await _certificateStore.ExistsAsync(thumbprint, token))
97+
{
98+
return;
99+
}
100+
101+
await _certificateStore.SaveAsync(certificate, token);
102+
}
103+
}
104+
}

src/Validation.PackageSigning.ExtractAndValidateSignature/SignatureValidator.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Security.Cryptography;
87
using System.Security.Cryptography.X509Certificates;
98
using System.Threading;
109
using System.Threading.Tasks;
@@ -20,15 +19,18 @@ namespace NuGet.Jobs.Validation.PackageSigning.ExtractAndValidateSignature
2019
public class SignatureValidator : ISignatureValidator
2120
{
2221
private readonly IPackageSigningStateService _packageSigningStateService;
22+
private readonly ISignaturePartsExtractor _signaturePartsExtractor;
2323
private readonly IEntityRepository<Certificate> _certificates;
2424
private readonly ILogger<SignatureValidator> _logger;
2525

2626
public SignatureValidator(
2727
IPackageSigningStateService packageSigningStateService,
28+
ISignaturePartsExtractor signaturePartsExtractor,
2829
IEntityRepository<Certificate> certificates,
2930
ILogger<SignatureValidator> logger)
3031
{
3132
_packageSigningStateService = packageSigningStateService ?? throw new ArgumentNullException(nameof(packageSigningStateService));
33+
_signaturePartsExtractor = signaturePartsExtractor ?? throw new ArgumentNullException(nameof(signaturePartsExtractor));
3234
_certificates = certificates ?? throw new ArgumentNullException(nameof(certificates));
3335
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3436
}
@@ -108,8 +110,11 @@ private async Task HandleSignedPackageAsync(
108110
message.ValidationId,
109111
packageThumbprints);
110112

113+
// Extract all of the signature artifacts and persist them.
114+
await _signaturePartsExtractor.ExtractAsync(signedPackageReader, cancellationToken);
115+
116+
// Mark this package as signed.
111117
await AcceptAsync(validation, message, PackageSigningStatus.Valid);
112-
return;
113118
}
114119

115120
private HashSet<string> GetThumbprints(IEnumerable<Signature> signatures)

src/Validation.PackageSigning.ExtractAndValidateSignature/Validation.PackageSigning.ExtractAndValidateSignature.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
<Reference Include="System.Security" />
4141
</ItemGroup>
4242
<ItemGroup>
43+
<Compile Include="Hash.cs" />
44+
<Compile Include="ISignaturePartsExtractor.cs" />
4345
<Compile Include="ISignatureValidator.cs" />
46+
<Compile Include="SignaturePartsExtractor.cs" />
4447
<Compile Include="SignatureValidator.cs" />
4548
<Compile Include="Storage\PackageSigningStateService.cs" />
4649
<Compile Include="Storage\IPackageSigningStateService.cs" />
@@ -98,7 +101,7 @@
98101
<Version>1.1.2</Version>
99102
</PackageReference>
100103
<PackageReference Include="NuGet.Packaging">
101-
<Version>4.6.0-preview2-4709</Version>
104+
<Version>4.6.0-preview3-4785</Version>
102105
</PackageReference>
103106
<PackageReference Include="NuGet.Services.Configuration">
104107
<Version>2.7.0</Version>

0 commit comments

Comments
 (0)