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

Commit 14121b5

Browse files
authored
Make Certificate Validator gracefully handle repository signed packages (#431)
The certificate validator expected a single author signature to be present if the package was marked as signed. This is no longer true as the package could be repository signed with no author signature.
1 parent 030e283 commit 14121b5

2 files changed

Lines changed: 142 additions & 10 deletions

File tree

src/NuGet.Services.Validation.Orchestrator/PackageSigning/ValidateCertificate/PackageCertificatesValidator.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,19 @@ private async Task<ValidatorStatus> GetStatusAsync(IValidationRequest request)
8686
// may have a status of "Unknown" if the package is at ingestion and its signature has passed
8787
// all validations, "Invalid" if one or more of the signature's certificates has failed validations,
8888
// or "InGracePeriod" or "Valid" if this is a revalidation request.
89-
var signature = await FindSignatureAsync(request);
89+
var signature = await FindAuthorSignatureAsync(request);
90+
91+
if (signature == null)
92+
{
93+
_logger.LogError(
94+
"Could not find author signature for {PackageKey} {PackageId} {PackageVersion} {ValidationId}",
95+
request.PackageKey,
96+
request.PackageId,
97+
request.PackageVersion,
98+
request.ValidationId);
99+
100+
throw new InvalidOperationException($"Package with key {request.PackageKey} does not have an author signature");
101+
}
90102

91103
if (signature.Status == PackageSignatureStatus.Invalid)
92104
{
@@ -160,12 +172,22 @@ private async Task<ValidatorStatus> StartInternalAsync(IValidationRequest reques
160172
return await _validatorStateService.TryAddValidatorStatusAsync(request, status, ValidationStatus.Failed);
161173
}
162174

163-
var isRevalidationRequest = await _validatorStateService.IsRevalidationRequestAsync(request);
175+
// Skip packages that are only repository signed.
176+
var signature = await FindAuthorSignatureAsync(request);
164177

165-
// Find the signatures used to sign the package and see if any certificates known to be revoked
166-
// invalidate any of these signatures. Note that a revoked certificate is assumed to remain
167-
// revoked forever.
168-
var signature = await FindSignatureAsync(request);
178+
if (signature == null)
179+
{
180+
_logger.LogInformation(
181+
"Package {PackageId} {PackageVersion} does not have an author signature, no additional validations necessary",
182+
request.PackageId,
183+
request.PackageVersion);
184+
185+
return await _validatorStateService.TryAddValidatorStatusAsync(request, status, ValidationStatus.Succeeded);
186+
}
187+
188+
// If any of the author signature's certificates are known to be revoked, invalidate any the signatures.
189+
// A revoked certificate is assumed to remain revoked forever.
190+
var isRevalidationRequest = await _validatorStateService.IsRevalidationRequestAsync(request);
169191

170192
if (ShouldInvalidateSignature(signature, isRevalidationRequest))
171193
{
@@ -226,18 +248,18 @@ private Task<PackageSigningState> FindPackageSigningStateAsync(IValidationReques
226248
}
227249

228250
/// <summary>
229-
/// Find all of the signatures and their certificates for the given validation request's package.
251+
/// Find the package's author signature, if one exists.
230252
/// </summary>
231253
/// <param name="request">The validation request containing the package whose signatures should be fetched.</param>
232-
/// <returns>The package's signatures with their certificates.</returns>
233-
private Task<PackageSignature> FindSignatureAsync(IValidationRequest request)
254+
/// <returns>The package's author signature with its certificates, or null.</returns>
255+
private Task<PackageSignature> FindAuthorSignatureAsync(IValidationRequest request)
234256
{
235257
return _validationContext
236258
.PackageSignatures
237259
.Include(s => s.EndCertificate)
238260
.Include(s => s.TrustedTimestamps.Select(t => t.EndCertificate))
239261
.Where(s => s.Type == PackageSignatureType.Author)
240-
.SingleAsync(s => s.PackageKey == request.PackageKey);
262+
.SingleOrDefaultAsync(s => s.PackageKey == request.PackageKey);
241263
}
242264

243265
/// <summary>

tests/NuGet.Services.Validation.Orchestrator.Tests/PackageSigning/ValidateCertificate/PackageCertificatesValidatorFacts.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,70 @@ public async Task ThrowsIfValidSignaturesHasTimestampWithRevokedCertificate()
536536

537537
Assert.Equal($"Package signature {signature.Key} is valid but has a timestamp whose end certificate is revoked\r\nParameter name: signature", ex.Message);
538538
}
539+
540+
[Fact]
541+
public async Task ThrowsIfPackageIsOnlyRepositorySigned()
542+
{
543+
var validatorStatus = new ValidatorStatus
544+
{
545+
ValidationId = ValidationId,
546+
ValidatorName = nameof(PackageCertificatesValidator),
547+
PackageKey = PackageKey,
548+
State = ValidationStatus.Incomplete,
549+
ValidatorIssues = new List<ValidatorIssue>(),
550+
};
551+
552+
var packageSigningState = new PackageSigningState
553+
{
554+
PackageKey = PackageKey,
555+
PackageId = PackageId,
556+
PackageNormalizedVersion = PackageNormalizedVersion,
557+
SigningStatus = PackageSigningStatus.Valid
558+
};
559+
560+
var signature = new PackageSignature
561+
{
562+
PackageKey = PackageKey,
563+
Status = PackageSignatureStatus.Unknown,
564+
Type = PackageSignatureType.Repository,
565+
};
566+
567+
var timestamp = new TrustedTimestamp
568+
{
569+
Value = DateTime.UtcNow.AddDays(-1)
570+
};
571+
572+
var signingCertificate = new EndCertificate
573+
{
574+
Status = EndCertificateStatus.Good,
575+
StatusUpdateTime = DateTime.UtcNow.AddSeconds(-1),
576+
};
577+
578+
var timestampCertificate = new EndCertificate
579+
{
580+
Status = EndCertificateStatus.Revoked,
581+
StatusUpdateTime = DateTime.UtcNow.AddSeconds(-1),
582+
RevocationTime = DateTime.UtcNow.AddSeconds(-1),
583+
};
584+
585+
packageSigningState.PackageSignatures = new[] { signature };
586+
signature.PackageSigningState = packageSigningState;
587+
signature.EndCertificate = signingCertificate;
588+
signature.TrustedTimestamps = new[] { timestamp };
589+
timestamp.PackageSignature = signature;
590+
timestamp.EndCertificate = timestampCertificate;
591+
592+
_validationContext.Mock(
593+
validatorStatuses: new[] { validatorStatus },
594+
packageSigningStates: new[] { packageSigningState },
595+
packageSignatures: new[] { signature },
596+
endCertificates: new[] { signature.EndCertificate, timestampCertificate });
597+
598+
// Act & Assert
599+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => _target.GetResultAsync(_validationRequest.Object));
600+
601+
Assert.Equal($"Package with key {validatorStatus.PackageKey} does not have an author signature", ex.Message);
602+
}
539603
}
540604

541605
public class TheStartValidationAsyncMethod : FactsBase
@@ -614,6 +678,52 @@ public async Task UnsignedPackagesAlwaysSucceed()
614678
Assert.Equal(ValidationStatus.Succeeded, validatorStatus.State);
615679
}
616680

681+
[Fact]
682+
public async Task OnlyRepositorySignedPackagesAlwaysSucceed()
683+
{
684+
var validatorStatus = new ValidatorStatus
685+
{
686+
ValidationId = ValidationId,
687+
PackageKey = PackageKey,
688+
ValidatorName = nameof(PackageCertificatesValidator),
689+
State = ValidationStatus.NotStarted,
690+
ValidatorIssues = new List<ValidatorIssue>(),
691+
};
692+
693+
var packageSigningState = new PackageSigningState
694+
{
695+
PackageKey = PackageKey,
696+
PackageId = PackageId,
697+
PackageNormalizedVersion = PackageNormalizedVersion,
698+
SigningStatus = PackageSigningStatus.Valid
699+
};
700+
701+
var packageSignature = new PackageSignature
702+
{
703+
PackageKey = PackageKey,
704+
Status = PackageSignatureStatus.Valid,
705+
Type = PackageSignatureType.Repository,
706+
};
707+
708+
// Arrange
709+
_validationContext.Mock(
710+
validatorStatuses: new[] { validatorStatus },
711+
packageSigningStates: new[] { packageSigningState },
712+
packageSignatures: new[] { packageSignature });
713+
714+
// Act & Assert
715+
var actual = await _target.StartAsync(_validationRequest.Object);
716+
717+
_certificateVerifier.Verify(v => v.EnqueueVerificationAsync(It.IsAny<IValidationRequest>(), It.IsAny<EndCertificate>()), Times.Never);
718+
_validationContext.Verify(c => c.SaveChangesAsync(), Times.Once);
719+
_telemetryService.Verify(
720+
x => x.TrackDurationToStartPackageCertificatesValidator(It.IsAny<TimeSpan>()),
721+
Times.Never);
722+
723+
Assert.Equal(ValidationStatus.Succeeded, actual.Status);
724+
Assert.Equal(ValidationStatus.Succeeded, validatorStatus.State);
725+
}
726+
617727
[Fact]
618728
public async Task ReturnsSucceededIfAllCertificatesAlreadyValidated()
619729
{

0 commit comments

Comments
 (0)