/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2009-2014 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.appclient.server.core; import com.sun.enterprise.deploy.shared.ArchiveFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import org.glassfish.api.deployment.DeploymentContext; import org.glassfish.api.deployment.archive.ReadableArchive; import org.glassfish.appclient.server.core.jws.JavaWebStartInfo; import org.glassfish.appclient.server.core.jws.servedcontent.ASJarSigner; import org.glassfish.appclient.server.core.jws.servedcontent.AutoSignedContent; import org.glassfish.appclient.server.core.jws.servedcontent.FixedContent; import org.glassfish.appclient.server.core.jws.servedcontent.StaticContent; import org.glassfish.hk2.api.ServiceLocator; /** * Records information about JARs from an EAR that are used by an * app client. Although JARs can be signed by multiple certificates, this * class ultimately associates each JAR with at most one alias with which * it was signed. (This is typically used by the Java Web Start support to * group like-signed JARs into the same generated JNLP. Java Web Start requires * that all JARs listed in a single JNLP document be signed by the same cert or * be unsigned.) By organizing the signed JARs by the signing alias used for each, * we can easily find all JARs signed by a given alias and then list them * in the same generated JNLP. * <p> * A client class should instantiate the manager, then invoke addJar any number * of times, then invoke aliasToContent to retrieve the map from each alias to * the corresponding (relativeURI, StaticContent) pair. * <p> * If an added JAR is already signed by the developer we do not sign it again, but * simply add it to the data structures. If an added JAR is not signed we * arrange for it to be auto-signed and the signed version will be the one * served to Java Web Start requests. * * @author tjquinn */ public class ApplicationSignedJARManager { private final Map<URI,Collection<String>> relURIToSigningAliases = new HashMap<URI,Collection<String>>(); private final Map<String,Collection<URI>> signingAliasToRelURIs = new HashMap<String,Collection<URI>>(); /** * maps each alias to a map. map entries link the relative URI (used as the * key in the Grizzly adapter's key-to-content map) to the StaticContent * instance for that JAR. In this map each served JAR is associated with * only one alias, even if a JAR is signed by multiple certs. */ private Map<String,Map<URI,StaticContent>> selectedAliasToContentMapping = null; private final ArchiveFactory archiveFactory; private final String autoSigningAlias; private final ASJarSigner jarSigner; private final URI EARDirectoryServerURI; private final DeploymentContext dc; private final AppClientDeployerHelper helper; private final Map<URI,StaticContent> relURIToContent = new HashMap<URI,StaticContent>(); public ApplicationSignedJARManager( final String autoSigningAlias, final ASJarSigner jarSigner, final ServiceLocator habitat, final DeploymentContext dc, final AppClientDeployerHelper helper, final URI EARDirectoryServerURI, final URI EARDirectoryUserURI) { this.autoSigningAlias = autoSigningAlias; this.jarSigner = jarSigner; this.EARDirectoryServerURI = EARDirectoryServerURI; archiveFactory = habitat.getService(ArchiveFactory.class); this.dc = dc; this.helper = helper; } /** * Adds a JAR to the manager, returning the URI for the file to be served. * The URI within the anchor is derived from the absolute URI relative * to the EAR's anchor on the server. * @param absJARURI absolute URI of the unsigned file. * @return URI to the file to be served * @throws IOException */ public URI addJAR(final URI absJARURI) throws IOException { final URI jarURIRelativeToApp = EARDirectoryServerURI.relativize(absJARURI); return addJAR(jarURIRelativeToApp, absJARURI); } /** * Adds a JAR to the manager, returning the URI to the file to be * served. This might be an auto-signed file if the original JAR is * unsigned. * @param uriWithinAnchor relative URI to the JAR within the anchor directory for the app * @param jarURI URI to the JAR file in the app to be served * @return URI to the JAR file to serve (either the original file or an auto-signed copy of the original) * @throws IOException */ public URI addJAR(final URI uriWithinAnchor, final URI absJARURI) throws IOException { /* * This method accomplishes three things: * * 1. Adds an entry to the map from relative URIs to the corresponding * static content for the JAR, creating an auto-signed content instance * if needed for an unsigned JAR. * * 2. Adds to the map from relative URI to aliases with which the JAR * is signed. * * 3. Adds to the map from alias to relative URIs signed with that alias. */ Map.Entry<URI,StaticContent> result; // relative URI -> StaticContent final ReadableArchive arch = archiveFactory.openArchive(absJARURI); final Manifest archiveMF = arch.getManifest(); if (archiveMF == null) { return null; } if ( ! isArchiveSigned(archiveMF)) { /* * The developer did not sign this JARs, so arrange for it to be * auto-signed. */ result = autoSignedAppContentEntry(uriWithinAnchor, absJARURI); updateAliasToURIs(result.getKey(), autoSigningAlias); updateURIToAliases(result.getKey(), autoSigningAlias); } else { /* * The developer did sign this JAR, possibly with many certs. * For each cert add an association between the signing alias and * the JAR. */ result = developerSignedAppContentEntry(absJARURI); Collection<String> aliasesUsedToSignJAR = new ArrayList<String>(); for (Enumeration<String> entryNames = arch.entries("META-INF/"); entryNames.hasMoreElements(); ) { final String entryName = entryNames.nextElement(); final String alias = signatureEntryName(entryName); updateURIToAliases(result.getKey(), alias); } addAliasToURIsEntry(result.getKey(), aliasesUsedToSignJAR); } arch.close(); return result.getKey(); } public Map<String,Map<URI,StaticContent>> aliasToContent() { if (selectedAliasToContentMapping == null) { selectedAliasToContentMapping = pruneMaps(); } return selectedAliasToContentMapping; } private void addAliasToURIsEntry(final URI relURI, final Collection<String> aliases) throws IOException { relURIToSigningAliases.put(relURI, aliases); for (String alias : aliases) { updateAliasToURIs(relURI, alias); } } private void updateURIToAliases(final URI relURI, final String alias) throws IOException { Collection<String> aliasesForJAR = relURIToSigningAliases.get(relURI); if (aliasesForJAR == null) { aliasesForJAR = new ArrayList<String>(); relURIToSigningAliases.put(relURI, aliasesForJAR); } aliasesForJAR.add(alias); } private void updateAliasToURIs(final URI relURI, final String alias) throws IOException { Collection<URI> urisForAlias = signingAliasToRelURIs.get(alias); if (urisForAlias == null) { urisForAlias = new ArrayList<URI>(); signingAliasToRelURIs.put(alias, urisForAlias); } urisForAlias.add(relURI); } private Map.Entry<URI,StaticContent> developerSignedAppContentEntry(URI absURIToFile) { final URI jarURIRelativeToApp = EARDirectoryServerURI.relativize(absURIToFile); StaticContent content = relURIToContent.get(absURIToFile); if (content == null) { content = new FixedContent(new File(absURIToFile)); relURIToContent.put(jarURIRelativeToApp, content); } return new AbstractMap.SimpleEntry<URI,StaticContent>( jarURIRelativeToApp, content); } public StaticContent staticContent(final URI jarURIRelativeToApp) { return relURIToContent.get(jarURIRelativeToApp); } /* * Returns information about an auto-signed JAR for a given absolute URI and * alias, creating the auto-signed content object and adding it to the * data structures if it is not already present. */ private synchronized Map.Entry<URI,StaticContent> autoSignedAppContentEntry( final URI jarURIRelativeToApp, final URI absURIToFile) throws FileNotFoundException { StaticContent content = relURIToContent.get(jarURIRelativeToApp); if (content == null) { final File unsignedFile = new File(absURIToFile); final File signedFile = signedFileForLib(jarURIRelativeToApp, unsignedFile); content = new AutoSignedContent(unsignedFile, signedFile, autoSigningAlias, jarSigner, jarURIRelativeToApp.toASCIIString(), helper.appName()); relURIToContent.put(jarURIRelativeToApp, content); } else { if (content instanceof AutoSignedContent) { content = AutoSignedContent.class.cast(content); } else { throw new RuntimeException(content.toString() + " != AutoSignedContent"); } } return new AbstractMap.SimpleEntry(jarURIRelativeToApp, content); } private File signedFileForLib(final URI relURI, final File unsignedFile) { return JavaWebStartInfo.signedFileForProvidedAppFile(relURI, unsignedFile, helper, dc); } /** * Returns the signature file name (no path, no suffix) if the specified * entry name matches the pattern of a signature file in a JAR. * @param entryName name to check * @return signature file name; null if the entry name does not match the pattern */ private String signatureEntryName(final String entryName) { final int firstSlash = entryName.indexOf('/'); final int lastSlash = entryName.lastIndexOf('/'); return ((entryName.startsWith("META-INF/") && firstSlash == lastSlash && firstSlash != -1) && (entryName.endsWith(".SF"))) ? entryName.substring(firstSlash + 1, entryName.indexOf(".SF")) : null; } private boolean isArchiveSigned(final Manifest archiveMF) throws IOException { /* * Signature files are *.SF, but looking through all the entries for * ones that match *.SF could be expensive if there are many entries. * Instead check the manifest to * see if it contains per-entry attributes and, if so, if the first * entry has a x-Digest-y entry-level attribute. */ final Map<String,Attributes> perEntryAttrs = archiveMF.getEntries(); boolean jarIsSigned = false; for (Map.Entry<String,Attributes> entry : perEntryAttrs.entrySet()) { for (Object attrKey : entry.getValue().keySet()) { if (attrKey.toString().contains("-Digest-") || attrKey.toString().contains("-Digest:")) { jarIsSigned = true; break; } } /* * We need to look only at the first entry because every entry * of a JAR file is recorded as signed in the manifest. */ break; } return jarIsSigned; } private Map<String,Map<URI,StaticContent>> pruneMaps() { /* * We'll eventually generate possibly multiple JNLP documents, one for each * different signing cert and each listing the JARs signed using that cert. * Java Web Start prompts end users for each untrusted cert. that was * used to sign the JARs in a JNLP. During deployment (which is when this code runs) * we cannot tell what certs or trusted authorities might be on an * end-users's system. So we would like to minimize the number of * different JNLPs which might help reduce the number of prompts the * user will see. * * We have a relationship between JARs and signing aliases. Ideally * we'd truly minimize the number of aliases but that's a hard (i.e., * computationally complex) problem and it's unlikely that real apps will contain * JARs signed by large numbers of different certs. So we'll do as * good a job as we can, but quickly. * * If a JAR is signed by exactly one cert then we must include that cert * and we might as well associate any other JARs signed by that cert * and others with that cert. Then we'll process any * unprocessed JARs by choosing for each the alias with which it was * signed with the largest number of other JARs also signed by that * alias. This is not guaranteed to be optimal but it should be * pretty good and will be fast. */ final Set<URI> processedJARs = new HashSet<URI>(); final Map<String,Map<URI,StaticContent>> selectedAliases = new HashMap<String,Map<URI,StaticContent>>(); for (Map.Entry<URI,Collection<String>> entry : relURIToSigningAliases.entrySet()) { if ( ! processedJARs.contains(entry.getKey())) { if (entry.getValue().size() == 1) { processURI(processedJARs, selectedAliases, entry.getKey(), entry.getValue().iterator().next()); } } } /* * We've handled all JARs that have just one signing alias. Now process * any remaining JARs. */ for (Map.Entry<URI,Collection<String>> entry : relURIToSigningAliases.entrySet()) { if ( ! processedJARs.contains(entry.getKey())) { processURI(processedJARs, selectedAliases, entry.getKey(), entry.getValue()); } } return selectedAliases; } private void processURI(final Set<URI> processedJARs, final Map<String,Map<URI,StaticContent>> selectedAliases, final URI relURI, final String alias) { Map<URI,StaticContent> urisForSelectedAlias = selectedAliases.get(alias); if (urisForSelectedAlias == null) { urisForSelectedAlias = new HashMap<URI,StaticContent>(); selectedAliases.put(alias, urisForSelectedAlias); } /* * Add this URI to the URIs to be associated with the specified alias. */ urisForSelectedAlias.put(relURI, relURIToContent.get(relURI)); /* * Record that we've processed this URI so we don't do so again. */ processedJARs.add(relURI); /* * Now that we know we need to handle this alias, mark all other JARs * that are associated with this alias (and perhaps others) to be * finally grouped with this alias alone. */ for (URI otherURI : signingAliasToRelURIs.get(alias)) { urisForSelectedAlias.put(otherURI, relURIToContent.get(otherURI)); processedJARs.add(otherURI); } } private void processURI(final Set<URI> processedJARs, final Map<String,Map<URI,StaticContent>> selectedAliases, final URI uri, final Collection<String> aliases) { /* * The algorithm we use to choose which of the multiple aliases to use * for this JAR could be anything. We'll just choose the first one. */ processURI(processedJARs, selectedAliases, uri, aliases.iterator().next()); } }