| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.ignite.spi.deployment.uri; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.security.CodeSigner; |
| import java.security.PublicKey; |
| import java.security.cert.Certificate; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.jar.Attributes; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.jar.JarInputStream; |
| import java.util.jar.Manifest; |
| import org.apache.ignite.IgniteLogger; |
| import org.apache.ignite.internal.util.typedef.internal.U; |
| |
| /** |
| * Helper class that verifies either JAR file or JAR file input stream |
| * if it is consistent or not. Consistency means that file was not changed |
| * since build and all files mentioned in manifest are signed. |
| */ |
| final class GridUriDeploymentJarVerifier { |
| /** |
| * Enforces singleton. |
| */ |
| private GridUriDeploymentJarVerifier() { |
| // No-op. |
| } |
| |
| /** Default buffer size = 4K. */ |
| private static final int BUF_SIZE = 4096; |
| |
| /** |
| * Verify JAR-file that it was not changed since creation. |
| * If parameter {@code allSigned} equals {@code true} and file is not |
| * listed in manifest than method return {@code false}. If file listed |
| * in manifest but doesn't exist in JAR-file than method return |
| * {@code false}. |
| * |
| * @param jarName JAR file name. |
| * @param allSigned If {@code true} then all files must be signed. |
| * @param log Logger. |
| * @return {@code true} if JAR file was not changed. |
| * @throws IOException Thrown if JAR file or its entries could not be read. |
| */ |
| static boolean verify(String jarName, boolean allSigned, IgniteLogger log) throws IOException { |
| assert jarName != null; |
| |
| return verify0(jarName, null, allSigned, log); |
| } |
| |
| /** |
| * Verify JAR-file that all files declared in manifest are signed. |
| * If manifest is {@code null} than method returns {@code true} if |
| * public key is {@code null}. |
| * If parameter {@code allSigned} equals {@code true} and file not |
| * listed in manifest than method return {@code false}. If file |
| * listed in manifest but doesn't exist in JAR-file than method |
| * return {@code false}. |
| * |
| * @param jarName JAR file name. |
| * @param pubKey Public key. |
| * @param allSigned If {@code true} then all files must be signed. |
| * @param log Logger. |
| * @return {@code true} if JAR file is signed with given public key. |
| * @throws IOException Thrown if JAR file or its entries could not be read. |
| */ |
| static boolean verify(String jarName, PublicKey pubKey, boolean allSigned, IgniteLogger log) |
| throws IOException { |
| assert jarName != null; |
| assert pubKey != null; |
| |
| return verify0(jarName, pubKey, allSigned, log); |
| } |
| |
| /** |
| * Tests whether given JAR file input stream was not changed since creation. |
| * |
| * @param in JAR file input stream. |
| * @param allSigned Hint which means that all files of all entries must be |
| * signed. |
| * @param log Logger. |
| * @return {@code true} if JAR file input stream was not changed. |
| * @throws IOException Thrown if JAR file stream or its entries could not |
| * be read. |
| */ |
| static boolean verify(InputStream in, boolean allSigned, IgniteLogger log) throws IOException { |
| assert in != null; |
| |
| return verify0(in, null, allSigned, log); |
| } |
| |
| /** |
| * Tests whether given JAR file input stream is signed with public key. |
| * If manifest is {@code null} than method returns {@code true} if |
| * public key is {@code null}. |
| * If parameter {@code allSigned} equals {@code true} and file not |
| * listed in manifest than method return {@code false}. If file |
| * listed in manifest but doesn't exist in JAR-file than method |
| * return {@code false}. |
| * |
| * @param in JAR file input stream. |
| * @param pubKey Public key to be tested with. |
| * @param allSigned Hint which means that all files in entry must be signed. |
| * @param log Logger. |
| * @return {@code true} if JAR file is signed with given public key. |
| * @throws IOException Thrown if JAR file or its entries could not be read. |
| */ |
| static boolean verify(InputStream in, PublicKey pubKey, boolean allSigned, IgniteLogger log) |
| throws IOException { |
| assert in != null; |
| assert pubKey != null; |
| |
| return verify0(in, pubKey, allSigned, log); |
| } |
| |
| /** |
| * Tests whether all files in given JAR file input stream are signed |
| * with public key. If manifest is {@code null} than method returns |
| * {@code true} if public key is null. |
| * |
| * @param in JAR file input stream. |
| * @param pubKey Public key to be tested with. |
| * @param allSigned Hint which means that all files in entry must be signed. |
| * @param log Logger. |
| * @return {@code true} if JAR file is signed with given public key. |
| * @throws IOException Thrown if JAR file or its entries could not be read. |
| */ |
| private static boolean verify0(InputStream in, PublicKey pubKey, boolean allSigned, IgniteLogger log) |
| throws IOException { |
| assert in != null; |
| |
| JarInputStream jin = null; |
| |
| try { |
| jin = new JarInputStream(in, true); |
| |
| Manifest manifest = jin.getManifest(); |
| |
| // Manifest must be included into a signed package. |
| if (manifest == null) |
| return pubKey == null; |
| |
| Set<String> manifestFiles = getSignedFiles(manifest); |
| |
| JarEntry jarEntry; |
| |
| while ((jarEntry = jin.getNextJarEntry()) != null) { |
| if (jarEntry.isDirectory()) |
| continue; |
| |
| // Verify by reading the file if altered. |
| // Will return quietly if no problem. |
| verifyDigestsImplicitly(jin); |
| |
| if (!verifyEntry(jarEntry, manifest, pubKey, allSigned, true)) |
| return false; |
| |
| manifestFiles.remove(jarEntry.getName()); |
| } |
| |
| return manifestFiles.size() <= 0; |
| } |
| catch (SecurityException e) { |
| if (log.isDebugEnabled()) |
| log.debug("Got security error (ignoring): " + e.getMessage()); |
| } |
| finally { |
| U.close(jin, log); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Tests whether all files in given JAR file are signed |
| * with public key. If manifest is {@code null} than method returns |
| * {@code true} if public key is null. |
| * <p> |
| * <strong>DO NOT REFACTOR THIS METHOD. THERE IS A SUN DEFECT ABOUT PROCESSING JAR AS |
| * FILE AND AS STREAM. THE PROCESSING IS DIFFERENT.</strong> |
| * |
| * @param jarName JAR file name. |
| * @param pubKey Public key to be tested with. |
| * @param allSigned Hint which means that all files in entry must be signed. |
| * @param log Logger. |
| * @return {@code true} if JAR file is signed with given public key. |
| * @throws IOException Thrown if JAR file or its entries could not be read. |
| */ |
| private static boolean verify0(String jarName, PublicKey pubKey, boolean allSigned, IgniteLogger log) |
| throws IOException { |
| JarFile jarFile = null; |
| |
| try { |
| jarFile = new JarFile(jarName, true); |
| |
| Manifest manifest = jarFile.getManifest(); |
| |
| // Manifest must be included into a signed package. |
| if (manifest == null) |
| return pubKey == null; |
| |
| Set<String> manifestFiles = getSignedFiles(manifest); |
| |
| Enumeration<JarEntry> entries = jarFile.entries(); |
| |
| while (entries.hasMoreElements()) { |
| JarEntry jarEntry = entries.nextElement(); |
| |
| if (jarEntry.isDirectory()) |
| continue; |
| |
| // Verify by reading the file if altered. |
| // Will return quietly if no problem. |
| verifyDigestsImplicitly(jarFile.getInputStream(jarEntry)); |
| |
| if (!verifyEntry(jarEntry, manifest, pubKey, allSigned, false)) |
| return false; |
| |
| manifestFiles.remove(jarEntry.getName()); |
| } |
| |
| return manifestFiles.size() <= 0; |
| } |
| catch (SecurityException e) { |
| if (log.isDebugEnabled()) |
| log.debug("Got security error (ignoring): " + e.getMessage()); |
| } |
| finally { |
| U.close(jarFile, log); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Tests whether given JAR entry from manifest contains at least one |
| * certificate with given public key. |
| * <p> |
| * Files which starts with "META-INF/" are always verified successfully. |
| * |
| * @param jarEntry Tested JAR entry. |
| * @param manifest Manifest this entry belongs to. |
| * @param pubKey Public key we are testing. If it is {@code null} returns |
| * {@code true}. |
| * @param allSigned Hint which means that all files in entry must be signed. |
| * @param makeCerts If {@code true} JAR entry certificates are scanned. |
| * Otherwise all JAR entry signers certificates are scanned. |
| * @return {@code true} if JAR entry is verified {@code false} otherwise. |
| */ |
| private static boolean verifyEntry(JarEntry jarEntry, Manifest manifest, PublicKey pubKey, boolean allSigned, |
| boolean makeCerts) { |
| assert jarEntry != null; |
| assert manifest != null; |
| |
| boolean inManifest = false; |
| |
| String entryName = jarEntry.getName(); |
| |
| // Check that entry name contains in manifest file. |
| if (manifest.getAttributes(entryName) != null || manifest.getAttributes("./" + entryName) != null || |
| manifest.getAttributes('/' + entryName) != null) |
| inManifest = true; |
| |
| // Don't ignore files not listed in manifest and META-INF directory. |
| if (allSigned && !inManifest && !entryName.toUpperCase().startsWith("META-INF/")) |
| return false; |
| |
| // Looking at entries in manifest file. |
| if (inManifest) { |
| Certificate[] certs = !makeCerts ? jarEntry.getCertificates() : getCertificates(jarEntry); |
| |
| boolean isSigned = certs != null && certs.length > 0; |
| |
| if (!isSigned || pubKey != null && !findKeyInCertificates(pubKey, certs)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * This checks that everything is valid and unchanged from the digest |
| * listed in the manifest next to the name. |
| * |
| * @param in JAR file or JAR entry input stream. |
| * @throws IOException Thrown if read fails. |
| */ |
| private static void verifyDigestsImplicitly(InputStream in) throws IOException { |
| byte[] buf = new byte[BUF_SIZE]; |
| |
| while (in.read(buf, 0, buf.length) != -1) { |
| // Just read the entry. Will throw a SecurityException if signature |
| // or digest check fails. Since we instantiated JarFile with parameter |
| // true, that tells it to verify that the files match the digests |
| // and haven't been changed. |
| } |
| } |
| |
| /** |
| * Tests whether given certificate contains public key or not. |
| * |
| * @param key Public key which we are looking for. |
| * @param certs Certificate which should be tested. |
| * @return {@code true} if certificate contains given key and |
| * {@code false} if not. |
| */ |
| private static boolean findKeyInCertificates(PublicKey key, Certificate[] certs) { |
| if (key == null || certs == null) |
| return false; |
| |
| for (Certificate cert : certs) { |
| if (cert.getPublicKey().equals(key)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Gets all signed files from the manifest. |
| * <p> |
| * It scans all manifest entries and their attributes. If there is an attribute |
| * name which ends with "-DIGEST" we are assuming that manifest entry name is a |
| * signed file name. |
| * |
| * @param manifest JAR file manifest. |
| * @return Either empty set if none found or set of signed file names. |
| */ |
| private static Set<String> getSignedFiles(Manifest manifest) { |
| Set<String> fileNames = new HashSet<>(); |
| |
| Map<String, Attributes> entries = manifest.getEntries(); |
| |
| if (entries != null && entries.size() > 0) { |
| for (Map.Entry<String, Attributes> entry : entries.entrySet()) { |
| Attributes attrs = entry.getValue(); |
| |
| for (Map.Entry<Object, Object> attrEntry : attrs.entrySet()) { |
| if (attrEntry.getKey().toString().toUpperCase().endsWith("-DIGEST")) { |
| fileNames.add(entry.getKey()); |
| |
| break; |
| } |
| } |
| } |
| } |
| |
| return fileNames; |
| } |
| |
| /** |
| * Gets all JAR file entry certificates. |
| * Method scans entry for signers and than collects all their certificates. |
| * |
| * @param entry JAR file entry. |
| * @return Array of certificates which corresponds to the entry. |
| */ |
| private static Certificate[] getCertificates(JarEntry entry) { |
| Certificate[] certs = null; |
| |
| CodeSigner[] signers = entry.getCodeSigners(); |
| |
| // Extract the certificates in each code signer's cert chain. |
| if (signers != null) { |
| List<Certificate> certChains = new ArrayList<>(); |
| |
| for (CodeSigner signer : signers) |
| certChains.addAll(signer.getSignerCertPath().getCertificates()); |
| |
| // Convert into a Certificate[] |
| return certChains.toArray(new Certificate[certChains.size()]); |
| } |
| |
| return certs; |
| } |
| } |