/// <summary> /// The helper that processes how a certificate's status change affects its dependent signatures. /// </summary> /// <param name="certificate">The certificate whose dependent signatures should be processed.</param> /// <param name="certificateVerificationResult">The result of the certificate's verification.</param> /// <param name="signatureDecider">The delegate that decides how a dependent signature should be handled.</param> /// <param name="onAllSignaturesHandled">The action that will be called once all dependent signatures have been processed.</param> /// <returns></returns> private async Task ProcessDependentSignaturesAsync( EndCertificate certificate, CertificateVerificationResult certificateVerificationResult, SignatureDecider signatureDecider, Action onAllSignaturesHandled) { // A single certificate may be dependend on by many signatures. To ensure sanity, only up // to "MaxSignatureUpdatesPerTransaction" signatures will be invalidated at a time. List <PackageSignature> signatures = null; int page = 0; do { // If necessary, save the previous iteration's signature invalidations. if (page > 0) { _logger.LogInformation( "Persisting {Signatures} dependent signature updates for certificate {CertificateThumbprint} (page {Page})", signatures.Count, certificate.Thumbprint, page); await _context.SaveChangesAsync(); page++; } _logger.LogInformation( "Finding more dependent signatures to update for certificate {CertificateThumbprint}... (page {Page})", certificate.Thumbprint, page); signatures = await FindSignaturesAsync(certificate, page); _logger.LogInformation( "Updating {Signatures} signatures for certificate {CertificateThumbprint}... (page {Page})", signatures.Count, certificate.Thumbprint, page); foreach (var signature in signatures) { var decision = signatureDecider(signature); HandleSignatureDecision(signature, decision, certificate, certificateVerificationResult); } }while (signatures.Count == MaxSignatureUpdatesPerTransaction); // All signatures have been invalidated. Do any necessary finalizations, and persist the results. _logger.LogInformation( "Finalizing {Signatures} dependent signature updates for certificate {CertificateThumbprint} (total pages: {Pages})", signatures.Count, certificate.Thumbprint, page + 1); onAllSignaturesHandled(); await _context.SaveChangesAsync(); }
/// <summary> /// Handle the decision on how to update the signature. /// </summary> /// <param name="signature">The signature that should be updated.</param> /// <param name="decision">How the signature should be updated.</param> /// <param name="certificate">The certificate that signature depends on that changed the signature's state.</param> /// <param name="certificateVerificationResult">The certificate verification that changed the signature's state.</param> private void HandleSignatureDecision( PackageSignature signature, SignatureDecision decision, EndCertificate certificate, CertificateVerificationResult certificateVerificationResult) { switch (decision) { case SignatureDecision.Ignore: _logger.LogInformation( "Signature {SignatureKey} is not affected by certificate verification result: {CertificateVerificationResult}", signature.Key, certificateVerificationResult); break; case SignatureDecision.Warn: _logger.LogWarning( "Invalidating signature {SignatureKey} due to certificate verification result: {CertificateVerificationResult}", signature.Key, certificateVerificationResult); InvalidateSignature(signature, certificate); _telemetryService.TrackPackageSignatureMayBeInvalidatedEvent(signature); break; case SignatureDecision.Reject: _logger.LogWarning( "Rejecting signature {SignatureKey} due to certificate verification result: {CertificateVerificationResult}", signature.Key, certificateVerificationResult); InvalidateSignature(signature, certificate); _telemetryService.TrackPackageSignatureShouldBeInvalidatedEvent(signature); break; default: throw new InvalidOperationException( $"Unknown signature decision '{decision}' for certificate verification result: {certificateVerificationResult}"); } }
private bool HasValidationCompleted(EndCertificateValidation validation, CertificateVerificationResult result) { // The validation is complete if the certificate was determined to be "Good", "Invalid", or "Revoked". if (result.Status == EndCertificateStatus.Good || result.Status == EndCertificateStatus.Invalid || result.Status == EndCertificateStatus.Revoked) { return(true); } else if (result.Status == EndCertificateStatus.Unknown) { // Certificates whose status failed to be determined will have an "Unknown" // status. These certificates should be retried until "_maximumValidationFailures" // is reached. if (validation.EndCertificate.ValidationFailures >= _maximumValidationFailures) { _logger.LogWarning( "Certificate {CertificateThumbprint} has reached maximum of {MaximumValidationFailures} failed validation attempts", validation.EndCertificate.Thumbprint, _maximumValidationFailures); return(true); } else { _logger.LogWarning( "Could not validate certificate {CertificateThumbprint}, {RetriesLeft} retries left", validation.EndCertificate.Thumbprint, _maximumValidationFailures - validation.EndCertificate.ValidationFailures); return(false); } } _logger.LogError( $"Unknown {nameof(EndCertificateStatus)} value: {{CertificateStatus}}, throwing to retry", result.Status); throw new InvalidOperationException($"Unknown {nameof(EndCertificateStatus)} value: {result.Status}"); }
/// <summary> /// Create a function to decide how signatures should be affected by an invalidated certificate. /// </summary> /// <param name="certificate">The certificate that was invalidated.</param> /// <param name="result">The verification result that describes why the certificate was invalidated.</param> /// <returns>The function that describes how dependent signatures should be affected by the certificate status change.</returns> public SignatureDecider MakeDeciderForInvalidatedCertificate(EndCertificate certificate, CertificateVerificationResult result) { if (result.Status != EndCertificateStatus.Invalid) { throw new ArgumentException($"Result must have a status of {nameof(EndCertificateStatus.Invalid)}", nameof(result)); } if (result.StatusFlags == X509ChainStatusFlags.NoError) { throw new ArgumentException($"Invalid flags on invalid verification result: {result.StatusFlags}!", nameof(result)); } // If a certificate used for the primary signature is revoked, all dependent signatures should be invalidated. // NOTE: It is assumed that the revoked certificate is an ancestor certificate, but this may not be strictly true. if (certificate.Use == EndCertificateUse.CodeSigning && (result.StatusFlags & X509ChainStatusFlags.Revoked) != 0) { return(RejectAllSignaturesDecider); } // NotTimeValid and HasWeakSignature fail packages only at ingestion. It is assumed that a chain with HasWeakSignature will // ALWAYS have NotSignatureValid. else if (result.StatusFlags == X509ChainStatusFlags.NotTimeValid || result.StatusFlags == (X509ChainStatusFlags.HasWeakSignature | X509ChainStatusFlags.NotSignatureValid) || result.StatusFlags == (X509ChainStatusFlags.NotTimeValid | X509ChainStatusFlags.HasWeakSignature | X509ChainStatusFlags.NotSignatureValid)) { return(RejectSignaturesAtIngestionDecider); } // NotTimeNested does not affect signatures and should be ignored if it is the only status. else if (result.StatusFlags == X509ChainStatusFlags.NotTimeNested) { return(NoActionDecider); } // In all other cases, reject signatures at ingestion and warn on all other signatures. else { return(RejectSignaturesAtIngestionOtherwiseWarnDecider); } }
/// <summary> /// Create a function to decide how signatures should be affected by a revoked certificate. /// </summary> /// <param name="certificate">The certificate that was revoked.</param> /// <param name="result">The verification result that describes when the certificate was revoked.</param> /// <returns>The function that describes how dependent signatures should be affected by the certificate status change.</returns> public SignatureDecider MakeDeciderForRevokedCertificate(EndCertificate certificate, CertificateVerificationResult result) { switch (certificate.Use) { case EndCertificateUse.Timestamping: return(RejectSignaturesAtIngestionOtherwiseWarnDecider); case EndCertificateUse.CodeSigning: return(MakeDeciderForRevokedCodeSigningCertificate(result.RevocationTime)); default: throw new InvalidOperationException($"Revoked certificate has unknown use: {certificate.Use}"); } }
public async Task <bool> TrySaveResultAsync(EndCertificateValidation validation, CertificateVerificationResult result) { if (validation.EndCertificate.Status == EndCertificateStatus.Revoked && result.Status != EndCertificateStatus.Revoked) { _logger.LogWarning( "Updating previously revoked certificate {CertificateThumbprint} to status {NewStatus}", validation.EndCertificate.Thumbprint, result.Status); } else { _logger.LogInformation( "Updating certificate {CertificateThumbprint} to status {NewStatus}", validation.EndCertificate.Thumbprint, result.Status); } try { switch (result.Status) { case EndCertificateStatus.Good: await SaveGoodCertificateStatusAsync(validation, result); break; case EndCertificateStatus.Unknown: await SaveUnknownCertificateStatusAsync(validation); break; case EndCertificateStatus.Invalid: await SaveInvalidCertificateStatusAsync(validation, result); break; case EndCertificateStatus.Revoked: await SaveRevokedCertificateStatusAsync(validation, result); break; default: _logger.LogError( $"Unknown {nameof(EndCertificateStatus)} value: {{CertificateStatus}}, throwing to retry", result.Status); throw new NotSupportedException($"Unknown {nameof(EndCertificateStatus)} value: {result.Status}"); } return(true); } catch (DbUpdateConcurrencyException e) { // The update concurrency exception be triggered by either the Certificate record or one of the dependent // PackageSignature records. Regardless, retry the validation so that the Certificate is validated with // the new state. _logger.LogWarning( 0, e, "Failed to update certificate {CertificateThumbprint} to status {NewStatus} due to concurrency exception", validation.EndCertificate.Thumbprint, result.Status); return(false); } }
private Task SaveRevokedCertificateStatusAsync(EndCertificateValidation validation, CertificateVerificationResult result) { var invalidationDecider = _signatureDeciderFactory.MakeDeciderForRevokedCertificate(validation.EndCertificate, result); void RevokeCertificate() { validation.EndCertificate.Status = EndCertificateStatus.Revoked; validation.EndCertificate.StatusUpdateTime = result.StatusUpdateTime; validation.EndCertificate.NextStatusUpdateTime = null; validation.EndCertificate.LastVerificationTime = DateTime.UtcNow; validation.EndCertificate.RevocationTime = result.RevocationTime; validation.EndCertificate.ValidationFailures = 0; validation.Status = EndCertificateStatus.Revoked; } return(ProcessDependentSignaturesAsync( validation.EndCertificate, result, invalidationDecider, onAllSignaturesHandled: RevokeCertificate)); }
private Task SaveGoodCertificateStatusAsync(EndCertificateValidation validation, CertificateVerificationResult result) { validation.EndCertificate.Status = EndCertificateStatus.Good; validation.EndCertificate.StatusUpdateTime = result.StatusUpdateTime; validation.EndCertificate.NextStatusUpdateTime = null; validation.EndCertificate.LastVerificationTime = DateTime.UtcNow; validation.EndCertificate.RevocationTime = null; validation.EndCertificate.ValidationFailures = 0; validation.Status = EndCertificateStatus.Good; return(_context.SaveChangesAsync()); }