/// <summary> /// Throws an exception if validation fails. /// </summary> /// <param name="certificates">The certificates to be checked.</param> /// <exception cref="ServiceResultException">If certificate[0] cannot be accepted</exception> protected virtual async Task InternalValidate(X509Certificate2Collection certificates) { X509Certificate2 certificate = certificates[0]; // check for previously validated certificate. X509Certificate2 certificate2 = null; if (m_validatedCertificates.TryGetValue(certificate.Thumbprint, out certificate2)) { if (Utils.IsEqual(certificate2.RawData, certificate.RawData)) { return; } } CertificateIdentifier trustedCertificate = await GetTrustedCertificate(certificate); // get the issuers (checks the revocation lists if using directory stores). List <CertificateIdentifier> issuers = new List <CertificateIdentifier>(); bool isIssuerTrusted = await GetIssuers(certificates, issuers); // setup policy chain X509ChainPolicy policy = new X509ChainPolicy(); policy.RevocationFlag = X509RevocationFlag.EntireChain; policy.RevocationMode = X509RevocationMode.NoCheck; policy.VerificationFlags = X509VerificationFlags.NoFlag; foreach (CertificateIdentifier issuer in issuers) { if ((issuer.ValidationOptions & CertificateValidationOptions.SuppressRevocationStatusUnknown) != 0) { policy.VerificationFlags |= X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown; policy.VerificationFlags |= X509VerificationFlags.IgnoreCtlSignerRevocationUnknown; policy.VerificationFlags |= X509VerificationFlags.IgnoreEndRevocationUnknown; policy.VerificationFlags |= X509VerificationFlags.IgnoreRootRevocationUnknown; } // we did the revocation check in the GetIssuers call. No need here. policy.RevocationMode = X509RevocationMode.NoCheck; policy.ExtraStore.Add(issuer.Certificate); } // build chain. X509Chain chain = new X509Chain(); chain.ChainPolicy = policy; chain.Build(certificate); // check the chain results. CertificateIdentifier target = trustedCertificate; if (target == null) { target = new CertificateIdentifier(certificate); } for (int ii = 0; ii < chain.ChainElements.Count; ii++) { X509ChainElement element = chain.ChainElements[ii]; CertificateIdentifier issuer = null; if (ii < issuers.Count) { issuer = issuers[ii]; } // check for chain status errors. foreach (X509ChainStatus status in element.ChainElementStatus) { ServiceResult result = CheckChainStatus(status, target, issuer, (ii != 0)); if (ServiceResult.IsBad(result)) { // check untrusted certificates. if (trustedCertificate == null) { throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed); } throw new ServiceResultException(result); } } if (issuer != null) { target = issuer; } } bool issuedByCA = !Utils.CompareDistinguishedName(certificate.Subject, certificate.Issuer); // check if certificate issuer is trusted. if (issuedByCA && !isIssuerTrusted) { if (m_applicationCertificate == null || !Utils.IsEqual(m_applicationCertificate.RawData, certificate.RawData)) { throw ServiceResultException.Create( StatusCodes.BadCertificateUntrusted, "Certificate issuer is not trusted.\r\nSubjectName: {0}\r\nIssuerName: {1}", certificate.SubjectName.Name, certificate.IssuerName.Name); } } // check if certificate is trusted. if (trustedCertificate == null && !isIssuerTrusted) { if (m_applicationCertificate == null || !Utils.IsEqual(m_applicationCertificate.RawData, certificate.RawData)) { throw ServiceResultException.Create( StatusCodes.BadCertificateUntrusted, "Certificate is not trusted.\r\nSubjectName: {0}\r\nIssuerName: {1}", certificate.SubjectName.Name, certificate.IssuerName.Name); } } }
private static ServiceResult CheckChainStatus(X509ChainStatus status, CertificateIdentifier id, CertificateIdentifier issuer, bool isIssuer) { switch (status.Status) { case X509ChainStatusFlags.NotValidForUsage: { return(ServiceResult.Create( (isIssuer) ? StatusCodes.BadCertificateUseNotAllowed : StatusCodes.BadCertificateIssuerUseNotAllowed, "Certificate may not be used as an application instance certificate. {0}: {1}", status.Status, status.StatusInformation)); } case X509ChainStatusFlags.NoError: case X509ChainStatusFlags.OfflineRevocation: case X509ChainStatusFlags.InvalidBasicConstraints: case X509ChainStatusFlags.PartialChain: { break; } case X509ChainStatusFlags.UntrustedRoot: { // ignore this error because the root check is done // by looking the certificate up in the trusted issuer stores passed to the validator. // the ChainStatus uses the Windows trusted issuer stores. break; } case X509ChainStatusFlags.RevocationStatusUnknown: { if (issuer != null) { if ((issuer.ValidationOptions & CertificateValidationOptions.SuppressRevocationStatusUnknown) != 0) { break; } } // check for meaning less errors for self-signed certificates. if (id.Certificate != null && Utils.CompareDistinguishedName(id.Certificate.Subject, id.Certificate.Subject)) { break; } return(ServiceResult.Create( (isIssuer) ? StatusCodes.BadCertificateIssuerRevocationUnknown : StatusCodes.BadCertificateRevocationUnknown, "Certificate revocation status cannot be verified. {0}: {1}", status.Status, status.StatusInformation)); } case X509ChainStatusFlags.Revoked: { return(ServiceResult.Create( (isIssuer) ? StatusCodes.BadCertificateIssuerRevoked : StatusCodes.BadCertificateRevoked, "Certificate has been revoked. {0}: {1}", status.Status, status.StatusInformation)); } case X509ChainStatusFlags.NotTimeNested: { if (id != null && ((id.ValidationOptions & CertificateValidationOptions.SuppressCertificateExpired) != 0)) { break; } return(ServiceResult.Create( StatusCodes.BadCertificateIssuerTimeInvalid, "Certificate issuer validatity time does not overhas is expired or not yet valid. {0}: {1}", status.Status, status.StatusInformation)); } case X509ChainStatusFlags.NotTimeValid: { if (id != null && ((id.ValidationOptions & CertificateValidationOptions.SuppressCertificateExpired) != 0)) { break; } return(ServiceResult.Create( (isIssuer) ? StatusCodes.BadCertificateIssuerTimeInvalid : StatusCodes.BadCertificateTimeInvalid, "Certificate has is expired or not yet valid. {0}: {1}", status.Status, status.StatusInformation)); } default: { return(ServiceResult.Create( StatusCodes.BadCertificateInvalid, "Certificate validation failed. {0}: {1}", status.Status, status.StatusInformation)); } } return(null); }
/// <summary> /// Validates a certificate. /// </summary> /// <remarks> /// Each UA application may have a list of trusted certificates that is different from /// all other UA applications that may be running on the same machine. As a result, the /// certificate validator cannot rely completely on the Windows certificate store and /// user or machine specific CTLs (certificate trust lists). /// /// The validator constructs the trust chain for the certificate and follows the chain /// until it finds a certification that is in the application trust list. Non-fatal trust /// chain errors (i.e. certificate expired) are ignored if the certificate is in the /// application trust list. /// /// If no certificate in the chain is trusted then the validator will still accept the /// certification if there are no trust chain errors. /// /// The validator may be configured to ignore the application trust list and/or trust chain. /// </remarks> public virtual void Validate(X509Certificate2Collection chain) { X509Certificate2 certificate = chain[0]; try { lock (m_lock) { Task.Run(async() => { await InternalValidate(chain); }).Wait(); // add to list of validated certificates. m_validatedCertificates[certificate.Thumbprint] = certificate; } } catch (AggregateException ae) { foreach (ServiceResultException e in ae.InnerExceptions) { // check for errors that may be suppressed. switch (e.StatusCode) { case StatusCodes.BadCertificateHostNameInvalid: case StatusCodes.BadCertificateIssuerRevocationUnknown: case StatusCodes.BadCertificateIssuerTimeInvalid: case StatusCodes.BadCertificateIssuerUseNotAllowed: case StatusCodes.BadCertificateRevocationUnknown: case StatusCodes.BadCertificateTimeInvalid: case StatusCodes.BadCertificateUriInvalid: case StatusCodes.BadCertificateUseNotAllowed: case StatusCodes.BadCertificateUntrusted: { Utils.Trace("Cert Validate failed: {0}", (StatusCode)e.StatusCode); break; } default: { throw new ServiceResultException(e, StatusCodes.BadCertificateInvalid); } } // invoke callback. bool accept = false; lock (m_callbackLock) { if (m_CertificateValidation != null) { CertificateValidationEventArgs args = new CertificateValidationEventArgs(new ServiceResult(e), certificate); m_CertificateValidation(this, args); accept = args.Accept; } } // throw if rejected. if (!accept) { // write the invalid certificate to a directory if specified. lock (m_lock) { Utils.Trace((int)Utils.TraceMasks.Error, "Certificate '{0}' rejected. Reason={1}", certificate.Subject, (StatusCode)e.StatusCode); if (m_rejectedCertificateStore != null) { Utils.Trace((int)Utils.TraceMasks.Error, "Writing rejected certificate to directory: {0}", m_rejectedCertificateStore); SaveCertificate(certificate); } } throw new ServiceResultException(e, StatusCodes.BadCertificateInvalid); } // add to list of peers. lock (m_lock) { m_validatedCertificates[certificate.Thumbprint] = certificate; } } } }