blob: 2006f38e2a1ea3c85f4d0167f3870f6fdd1a1785 [file] [log] [blame]
/*
* 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;
}
}