Merge branch 'security-rc' into SECURITY-144-rc

Conflicts:
	core/src/main/java/hudson/FilePath.java
This commit is contained in:
Jesse Glick committed 2014-10-26 13:55:39 -07:00
commit f0658e233d
33 files changed
+461 -99

No files matched your search

+72
View File
@@ -89,6 +89,7 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.io.Writer;
import java.lang.reflect.Field;
@@ -1760,6 +1761,77 @@ public final class FilePath implements Serializable {
return p.getIn();
}
/**
* Reads this file from the specific offset.
*/
public InputStream readFromOffset(final long offset) throws IOException {
if(channel ==null) {
final RandomAccessFile raf = new RandomAccessFile(new File(remote), "r");
try {
raf.seek(offset);
} catch (IOException e) {
try {
raf.close();
} catch (IOException e1) {
// ignore
}
throw e;
}
return new InputStream() {
@Override
public int read() throws IOException {
return raf.read();
}
@Override
public void close() throws IOException {
raf.close();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return raf.read(b, off, len);
}
@Override
public int read(byte[] b) throws IOException {
return raf.read(b);
}
};
}
final Pipe p = Pipe.createRemoteToLocal();
channel.callAsync(new Callable<Void, IOException>() {
private static final long serialVersionUID = 1L;
public Void call() throws IOException {
final OutputStream out = new java.util.zip.GZIPOutputStream(p.getOut(), 8192);
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(new File(remote), "r");
raf.seek(offset);
byte[] buf = new byte[8192];
int len;
while ((len = raf.read(buf)) >= 0) {
out.write(buf, 0, len);
}
return null;
} finally {
IOUtils.closeQuietly(out);
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
// ignore
}
}
}
}
});
return new java.util.zip.GZIPInputStream(p.getIn());
}
/**
* Reads this file into a string, by using the current system encoding.
*/
+28
View File
@@ -56,6 +56,7 @@ import javax.xml.transform.TransformerFactoryConfigurationError;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Date;
@@ -116,6 +117,8 @@ public class WebAppMain implements ServletContextListener {
installLogger();
markCookieAsHttpOnly(context);
final FileAndDescription describedHomeDir = getHomeDir(event);
home = describedHomeDir.file.getAbsoluteFile();
home.mkdirs();
@@ -251,6 +254,31 @@ public class WebAppMain implements ServletContextListener {
}
}
/**
* Set the session cookie as HTTP only.
*
* @see <a href="https://www.owasp.org/index.php/HttpOnly">discussion of this topic in OWASP</a>
*/
private void markCookieAsHttpOnly(ServletContext context) {
try {
Method m;
try {
m = context.getClass().getMethod("getSessionCookieConfig");
} catch (NoSuchMethodException x) { // 3.0+
LOGGER.log(Level.FINE, "Failed to set secure cookie flag", x);
return;
}
Object sessionCookieConfig = m.invoke(context);
// not exposing session cookie to JavaScript to mitigate damage caused by XSS
Class scc = Class.forName("javax.servlet.SessionCookieConfig");
Method setHttpOnly = scc.getMethod("setHttpOnly",boolean.class);
setHttpOnly.invoke(sessionCookieConfig,true);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to set HTTP-only cookie flag", e);
}
}
public void joinInit() throws InterruptedException {
initThread.join();
}
+13 -7
View File
@@ -24,6 +24,8 @@
*/
package hudson.model;
import edu.umd.cs.findbugs.annotations.OverrideMustInvoke;
import edu.umd.cs.findbugs.annotations.When;
import hudson.EnvVars;
import hudson.Launcher.ProcStarter;
import hudson.Util;
@@ -108,14 +110,14 @@ import static javax.servlet.http.HttpServletResponse.*;
*
* <p>
* {@link Executor}s on one {@link Computer} are transparently interchangeable
* (that is the definition of {@link Computer}.)
* (that is the definition of {@link Computer}).
*
* <p>
* This object is related to {@link Node} but they have some significant difference.
* This object is related to {@link Node} but they have some significant differences.
* {@link Computer} primarily works as a holder of {@link Executor}s, so
* if a {@link Node} is configured (probably temporarily) with 0 executors,
* you won't have a {@link Computer} object for it (except for the master node,
* which always get its {@link Computer} in case we have no static executors and
* which always gets its {@link Computer} in case we have no static executors and
* we need to run a {@link FlyweightTask} - see JENKINS-7291 for more discussion.)
*
* Also, even if you remove a {@link Node}, it takes time for the corresponding
@@ -124,8 +126,8 @@ import static javax.servlet.http.HttpServletResponse.*;
* remains intact, while all the {@link Node} objects will go away.
*
* <p>
* This object also serves UI (since {@link Node} is an interface and can't have
* related side pages.)
* This object also serves UI (unlike {@link Node}), and can be used along with
* {@link TransientComputerActionFactory} to add {@link Action}s to {@link Computer}s.
*
* @author Kohsuke Kawaguchi
*/
@@ -207,7 +209,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* The method also creates a log directory if required.
* @see #relocateOldLogs()
*/
protected File getLogFile() {
public File getLogFile() {
File dir = new File(Jenkins.getInstance().getRootDir(),"logs/slaves/"+nodeName);
dir.mkdirs();
return new File(dir,"slave.log");
@@ -1318,9 +1320,13 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* scheduling that does not overlap with being offline.
*
* @return {@code true} if the computer is accepting tasks
* @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer)
* @see hudson.model.Node#isAcceptingTasks()
*/
@OverrideMustInvoke(When.ANYTIME)
public boolean isAcceptingTasks() {
return true;
final Node node = getNode();
return getRetentionStrategy().isAcceptingTasks(this) && (node == null || node.isAcceptingTasks());
}
/**
@@ -140,7 +140,7 @@ public class FileParameterValue extends ParameterValue {
return new BuildWrapper() {
@Override
public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
if (!StringUtils.isEmpty(location)) {
if (!StringUtils.isEmpty(location) && !StringUtils.isEmpty(file.getName())) {
listener.getLogger().println("Copying file to "+location);
FilePath locationFilePath = build.getWorkspace().child(location);
locationFilePath.getParent().mkdirs();
@@ -162,7 +162,8 @@ public class FileParameterValue extends ParameterValue {
}
/**
* In practice this will always be false, since location should be unique.
* Compares file parameters (existing files will be considered as different).
* @since 1.586 Function has been modified in order to avoid <a href="https://issues.jenkins-ci.org/browse/JENKINS-19017">JENKINS-19017</a> issue (wrong merge of builds in the queue).
*/
@Override
public boolean equals(Object obj) {
@@ -173,12 +174,13 @@ public class FileParameterValue extends ParameterValue {
if (getClass() != obj.getClass())
return false;
FileParameterValue other = (FileParameterValue) obj;
if (location == null) {
if (other.location != null)
return false;
} else if (!location.equals(other.location))
return false;
return true;
if (location == null && other.location == null)
return true; // Consider null parameters as equal
//TODO: check fingerprints or checksums to improve the behavior (JENKINS-25211)
// Return false even if files are equal
return false;
}
@Override
+19 -1
View File
@@ -69,7 +69,7 @@ import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Exported;
/**
* Base type of Jenkins slaves (although in practice, you probably extend {@link Slave} to define a new slave type.)
* Base type of Jenkins slaves (although in practice, you probably extend {@link Slave} to define a new slave type).
*
* <p>
* As a special case, {@link Jenkins} extends from here.
@@ -77,6 +77,10 @@ import org.kohsuke.stapler.export.Exported;
* <p>
* Nodes are persisted objects that capture user configurations, and instances get thrown away and recreated whenever
* the configuration changes. Running state of nodes are captured by {@link Computer}s.
*
* <p>
* There is no URL binding for {@link Node}. {@link Computer} and {@link TransientComputerActionFactory} must
* be used to associate new {@link Action}s to slaves.
*
* @author Kohsuke Kawaguchi
* @see NodeMonitor
@@ -190,6 +194,20 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
*/
protected abstract Computer createComputer();
/**
* Returns {@code true} if the node is accepting tasks. Needed to allow slaves programmatic suspension of task
* scheduling that does not overlap with being offline. Called by {@link Computer#isAcceptingTasks()}.
* This method is distinct from {@link Computer#isAcceptingTasks()} as sometimes the {@link Node} concrete
* class may not have control over the {@link hudson.model.Computer} concrete class associated with it.
*
* @return {@code true} if the node is accepting tasks.
* @see Computer#isAcceptingTasks()
* @since 1.586
*/
public boolean isAcceptingTasks() {
return true;
}
/**
* Let Nodes be aware of the lifecycle of their own {@link Computer}.
*/
+31 -21
View File
@@ -81,6 +81,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Represents a user.
@@ -191,11 +192,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
return id;
}
public String getUrl() {
public @Nonnull String getUrl() {
return "user/"+Util.rawEncode(idStrategy().keyFor(id));
}
public String getSearchUrl() {
public @Nonnull String getSearchUrl() {
return "/user/"+Util.rawEncode(idStrategy().keyFor(id));
}
@@ -203,24 +204,22 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* The URL of the user page.
*/
@Exported(visibility=999)
public String getAbsoluteUrl() {
public @Nonnull String getAbsoluteUrl() {
return Jenkins.getInstance().getRootUrl()+getUrl();
}
/**
* Gets the human readable name of this user.
* This is configurable by the user.
*
* @return
* never null.
*/
@Exported(visibility=999)
public String getFullName() {
public @Nonnull String getFullName() {
return fullName;
}
/**
* Sets the human readable name of thie user.
* Sets the human readable name of the user.
* If the input parameter is empty, the user's ID will be set.
*/
public void setFullName(String name) {
if(Util.fixEmptyAndTrim(name)==null) name=id;
@@ -228,7 +227,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
}
@Exported
public String getDescription() {
public @CheckForNull String getDescription() {
return description;
}
@@ -242,7 +241,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
/**
* Updates the user object by adding a property.
*/
public synchronized void addProperty(UserProperty p) throws IOException {
public synchronized void addProperty(@Nonnull UserProperty p) throws IOException {
UserProperty old = getProperty(p.getClass());
List<UserProperty> ps = new ArrayList<UserProperty>(properties);
if(old!=null)
@@ -284,7 +283,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* If this user is not a valid user in the backend {@link SecurityRealm}.
* @since 1.419
*/
public Authentication impersonate() throws UsernameNotFoundException {
public @Nonnull Authentication impersonate() throws UsernameNotFoundException {
try {
UserDetails u = new ImpersonatingUserDetailsService(
Jenkins.getInstance().getSecurityRealm().getSecurityComponents().userDetails).loadUserByUsername(id);
@@ -332,9 +331,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* (by creating a new {@link User} object if none exists.)
* If false, this method will return null if {@link User} object
* with the given name doesn't exist.
* @return Requested user. May be {@code null} if a user does not exist and
* {@code create} is false.
* @deprecated use {@link User#get(String, boolean, java.util.Map)}
*/
public static User get(String idOrFullName, boolean create) {
public static @Nullable User get(String idOrFullName, boolean create) {
return get(idOrFullName, create, Collections.emptyMap());
}
@@ -350,8 +351,12 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* @param context
* contextual environment this user idOfFullName was retrieved from,
* that can help resolve the user ID
*
* @return
* An existing or created user. May be {@code null} if a user does not exist and
* {@code create} is false.
*/
public static User get(String idOrFullName, boolean create, Map context) {
public static @Nullable User get(String idOrFullName, boolean create, Map context) {
if(idOrFullName==null)
return null;
@@ -369,14 +374,19 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
}
}
// DefaultUserCanonicalIdResolver will always return a non-null id if all other CanonicalIdResolver failed
if (id == null) {
throw new IllegalStateException("The user id should be always non-null thanks to DefaultUserCanonicalIdResolver");
}
return getOrCreate(id, idOrFullName, create);
}
/**
* retrieve a user by its ID, and create a new one if requested
* Retrieve a user by its ID, and create a new one if requested.
* @return
* An existing or created user. May be {@code null} if a user does not exist and
* {@code create} is false.
*/
private static User getOrCreate(String id, String fullName, boolean create) {
private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create) {
String idkey = idStrategy().keyFor(id);
byNameLock.readLock().lock();
@@ -468,7 +478,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
/**
* Gets all the users.
*/
public static Collection<User> getAll() {
public static @Nonnull Collection<User> getAll() {
final IdStrategy strategy = idStrategy();
if(System.currentTimeMillis() -lastScanned>10000) {
// occasionally scan the file system to check new users
@@ -555,12 +565,12 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
/**
* Returns the user name.
*/
public String getDisplayName() {
public @Nonnull String getDisplayName() {
return getFullName();
}
/** true if {@link AbstractBuild#hasParticipant} or {@link hudson.model.Cause.UserIdCause} */
private boolean relatedTo(AbstractBuild<?,?> b) {
private boolean relatedTo(@Nonnull AbstractBuild<?,?> b) {
if (b.hasParticipant(this)) {
return true;
}
@@ -580,7 +590,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* by the timestamp order.
*/
@WithBridgeMethods(List.class)
public RunList getBuilds() {
public @Nonnull RunList getBuilds() {
return new RunList<Run<?,?>>(Jenkins.getInstance().getAllItems(Job.class)).filter(new Predicate<Run<?,?>>() {
@Override public boolean apply(Run<?,?> r) {
return r instanceof AbstractBuild && relatedTo((AbstractBuild<?,?>) r);
@@ -592,7 +602,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* Gets all the {@link AbstractProject}s that this user has committed to.
* @since 1.191
*/
public Set<AbstractProject<?,?>> getProjects() {
public @Nonnull Set<AbstractProject<?,?>> getProjects() {
Set<AbstractProject<?,?>> r = new HashSet<AbstractProject<?,?>>();
for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class))
if(p.hasParticipant(this))
@@ -54,8 +54,11 @@ import javax.annotation.Nonnull;
* Extensible property of {@link Node}.
*
* <p>
* Plugins can contribute this extension point to add additional data or UI actions to {@link Node}.
* Plugins can contribute this extension point to add additional data to {@link Node}.
* {@link NodeProperty}s show up in the configuration screen of a node, and they are persisted with the {@link Node} object.
*
* <p>
* To add UI action to {@link Node}s, i.e. a new link shown in the left side menu on a node page (<code>./computer/&lt;a node&gt;</code>), see instead {@link hudson.model.TransientComputerActionFactory}.
*
*
* <h2>Views</h2>
@@ -182,7 +182,7 @@ public class NodeProvisioner {
cl.onComplete(f,node);
hudson.addNode(node);
LOGGER.info(f.displayName+" provisioningE successfully completed. We have now "+hudson.getComputers().length+" computer(s)");
LOGGER.info(f.displayName+" provisioning successfully completed. We have now "+hudson.getComputers().length+" computer(s)");
} catch (InterruptedException e) {
throw new AssertionError(e); // since we confirmed that the future is already done
} catch (ExecutionException e) {
@@ -67,6 +67,20 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
return true;
}
/**
* Returns {@code true} if the computer is accepting tasks. Needed to allow retention strategies programmatic
* suspension of task scheduling that in preparation for going offline. Called by
* {@link hudson.model.Computer#isAcceptingTasks()}
*
* @param c the computer.
* @return {@code true} if the computer is accepting tasks
* @see hudson.model.Computer#isAcceptingTasks()
* @since 1.586
*/
public boolean isAcceptingTasks(T c) {
return true;
}
/**
* Called when a new {@link Computer} object is introduced (such as when Hudson started, or when
* a new slave is added.)
@@ -23,6 +23,8 @@
*/
package hudson.slaves;
import edu.umd.cs.findbugs.annotations.OverrideMustInvoke;
import edu.umd.cs.findbugs.annotations.When;
import hudson.AbortException;
import hudson.remoting.ChannelBuilder;
import hudson.util.IOUtils;
@@ -141,8 +143,11 @@ public class SlaveComputer extends Computer {
* {@inheritDoc}
*/
@Override
@OverrideMustInvoke(When.ANYTIME)
public boolean isAcceptingTasks() {
return acceptingTasks;
// our boolean flag is an override on any additional programmatic reasons why this slave might not be
// accepting tasks.
return acceptingTasks && super.isAcceptingTasks();
}
/**
@@ -153,8 +158,11 @@ public class SlaveComputer extends Computer {
}
/**
* Allows a {@linkplain hudson.slaves.ComputerLauncher} or a {@linkplain hudson.slaves.RetentionStrategy} to
* suspend tasks being accepted by the slave computer.
* Allows suspension of tasks being accepted by the slave computer. While this could be called by a
* {@linkplain hudson.slaves.ComputerLauncher} or a {@linkplain hudson.slaves.RetentionStrategy}, such usage
* can result in fights between multiple actors calling setting differential values. A better approach
* is to override {@link hudson.slaves.RetentionStrategy#isAcceptingTasks(hudson.model.Computer)} if the
* {@link hudson.slaves.RetentionStrategy} needs to control availability.
*
* @param acceptingTasks {@code true} if the slave can accept tasks.
*/
@@ -297,6 +297,9 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep {
* Performs on-the-fly validation on the file mask wildcard.
*/
public FormValidation doCheckArtifacts(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException {
if (project == null) {
return FormValidation.ok();
}
return FilePath.validateFileMask(project.getSomeWorkspace(),value);
}
@@ -260,6 +260,9 @@ public class Fingerprinter extends Recorder implements Serializable, DependencyD
}
public FormValidation doCheckTargets(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String value) throws IOException {
if (project == null) {
return FormValidation.ok();
}
return FilePath.validateFileMask(project.getSomeWorkspace(),value);
}
@@ -14,6 +14,7 @@ import javax.mail.internet.InternetAddress;
import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -117,14 +118,17 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration {
}
Object sessionCookieConfig = m.invoke(context);
// not exposing session cookie to JavaScript to mitigate damage caused by XSS
Class scc = Class.forName("javax.servlet.SessionCookieConfig");
Method setHttpOnly = scc.getMethod("setHttpOnly",boolean.class);
setHttpOnly.invoke(sessionCookieConfig,true);
Method setSecure = scc.getMethod("setSecure",boolean.class);
Method setSecure = scc.getMethod("setSecure", boolean.class);
boolean v = fixNull(jenkinsUrl).startsWith("https");
setSecure.invoke(sessionCookieConfig,v);
setSecure.invoke(sessionCookieConfig, v);
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof IllegalStateException) {
// servlet 3.0 spec seems to prohibit this from getting set at runtime,
// though Winstone is happy to accept i. see JENKINS-25019
return;
}
LOGGER.log(Level.WARNING, "Failed to set secure cookie flag", e);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to set secure cookie flag", e);
}
@@ -13,11 +13,14 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
@@ -74,19 +77,61 @@ public class JSONSignatureValidator {
Set<TrustAnchor> anchors = new HashSet<TrustAnchor>(); // CertificateUtil.getDefaultRootCAs();
Jenkins j = Jenkins.getInstance();
for (String cert : (Set<String>) j.servletContext.getResourcePaths("/WEB-INF/update-center-rootCAs")) {
if (cert.endsWith(".txt")) continue; // skip text files that are meant to be documentation
anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(j.servletContext.getResourceAsStream(cert)),null));
if (cert.endsWith("/") || cert.endsWith(".txt")) {
continue; // skip directories also any text files that are meant to be documentation
}
InputStream in = j.servletContext.getResourceAsStream(cert);
if (in == null) continue; // our test for paths ending in / should prevent this from happening
Certificate certificate;
try {
certificate = cf.generateCertificate(in);
} catch (CertificateException e) {
LOGGER.log(Level.WARNING, String.format("Webapp resources in /WEB-INF/update-center-rootCAs are "
+ "expected to be either certificates or .txt files documenting the "
+ "certificates, but %s did not parse as a certificate. Skipping this "
+ "resource for now.",
cert), e);
continue;
} finally {
in.close();
}
try {
anchors.add(new TrustAnchor((X509Certificate) certificate, null));
} catch (IllegalArgumentException e) {
LOGGER.log(Level.WARNING,
String.format("The name constraints in the certificate resource %s could not be "
+ "decoded. Skipping this resource for now.",
cert), e);
}
}
File[] cas = new File(j.root, "update-center-rootCAs").listFiles();
if (cas!=null) {
for (File cert : cas) {
if (cert.getName().endsWith(".txt")) continue; // skip text files that are meant to be documentation
if (cert.isDirectory() || cert.getName().endsWith(".txt")) {
continue; // skip directories also any text files that are meant to be documentation
}
FileInputStream in = new FileInputStream(cert);
Certificate certificate;
try {
anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(in),null));
certificate = cf.generateCertificate(in);
} catch (CertificateException e) {
LOGGER.log(Level.WARNING, String.format("Files in %s are expected to be either "
+ "certificates or .txt files documenting the certificates, "
+ "but %s did not parse as a certificate. Skipping this file for now.",
cert.getParentFile().getAbsolutePath(),
cert.getAbsolutePath()), e);
continue;
} finally {
in.close();
}
try {
anchors.add(new TrustAnchor((X509Certificate) certificate, null));
} catch (IllegalArgumentException e) {
LOGGER.log(Level.WARNING,
String.format("The name constraints in the certificate file %s could not be "
+ "decoded. Skipping this file for now.",
cert.getAbsolutePath()), e);
}
}
}
CertificateUtil.validatePath(certs, anchors);