/*******************************************************************************
* Copyright (c) 2006, 2007 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.osgi.internal.verifier;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;
import java.util.*;
import org.eclipse.osgi.baseadaptor.bundlefile.BundleEntry;
import org.eclipse.osgi.baseadaptor.bundlefile.BundleFile;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.internal.provisional.verifier.*;
import org.eclipse.osgi.util.NLS;
/**
* This class wraps a Repository of classes and resources to check and enforce
* signatures. It requires full signing of the manifest by all signers. If no
* signatures are found, the classes and resources are retrieved without checks.
*/
public class SignedBundleFile extends BundleFile implements CertificateVerifier, JarVerifierConstant {
private static DefaultTrustAuthority trustAllAuthority = new DefaultTrustAuthority(0);
private BundleFile bundleFile;
CertificateChain[] chains;
/**
* The key of the hashtable will be the name of the entry (type String). The
* value will be MessageDigest algorithm to use.
*/
Hashtable digests4entries;
/**
* The key of the hashtable will be the name of the entry (type String). The
* value will be byte[] which is an array of one MessageDigest result.
*/
Hashtable results4entries;
String manifestSHAResult = null;
String manifestMD5Result = null;
boolean certsInitialized = false;
SignedBundleFile() {
// default constructor
}
SignedBundleFile(CertificateChain[] chains, Hashtable digests4entries, Hashtable results4entries, String manifestMD5Result, String manifestSHAResult) {
this.chains = chains;
this.digests4entries = digests4entries;
this.results4entries = results4entries;
this.manifestMD5Result = manifestMD5Result;
this.manifestSHAResult = manifestSHAResult;
certsInitialized = true;
// isSigned = true;
}
/**
* Verify the digest listed in each entry in the .SF file with corresponding section in the manifest
*/
private void verifyManifestAndSingatureFile(byte[] manifestBytes, byte[] sfBytes) {
String sf = new String(sfBytes);
sf = stripContinuations(sf);
// check if there -Digest-Manfiest: header in the file
int off = sf.indexOf(digestManifestSearch);
if (off != -1) {
int start = sf.lastIndexOf('\n', off);
String manfiestDigest = null;
if (start != -1) {
// Signature-Version has to start the file, so there
// should always be a newline at the start of
// Digest-Manifest
String digestName = sf.substring(start + 1, off);
if (digestName.equalsIgnoreCase(MD5_STR)) {
if (manifestMD5Result == null)
manifestMD5Result = calculateDigest(getMessageDigest(MD5_STR), manifestBytes);
manfiestDigest = manifestMD5Result;
} else if (digestName.equalsIgnoreCase(SHA1_STR)) {
if (manifestSHAResult == null)
manifestSHAResult = calculateDigest(getMessageDigest(SHA1_STR), manifestBytes);
manfiestDigest = manifestSHAResult;
}
off += digestManifestSearchLen;
// find out the index of first '\n' after the -Digest-Manifest:
int nIndex = sf.indexOf('\n', off);
String digestValue = sf.substring(off, nIndex - 1);
// check if the the computed digest value of manifest file equals to the digest value in the .sf file
if (!manfiestDigest.equals(digestValue)) {
Exception e = new SecurityException(NLS.bind(JarVerifierMessages.Security_File_Is_Tampered, new String[] {bundleFile.getBaseFile().toString()}));
SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
throw (SecurityException) e;
}
}
}
}
/**
* Read the .SF file abd assuming that same digest algorithm will be used through out the whole
* .SF file. That digest algorithm name in the last entry will be returned.
*
* @param SFBuf a .SF file in bytes
* @return the digest algorithm name used in the .SF file
*/
private String getDigAlgFromSF(byte SFBuf[]) {
String rtvValue = null;
// need to make a string from the MF file data bytes
String mfStr = new String(SFBuf);
String entryStr = null;
// start parsing each entry in the MF String
int entryStartOffset = mfStr.indexOf(MF_ENTRY_NEWLN_NAME);
int length = mfStr.length();
while ((entryStartOffset != -1) && (entryStartOffset < length)) {
// get the start of the next 'entry', i.e. the end of this entry
int entryEndOffset = mfStr.indexOf(MF_ENTRY_NEWLN_NAME, entryStartOffset + 1);
if (entryEndOffset == -1) {
// if there is no next entry, then the end of the string
// is the end of this entry
entryEndOffset = mfStr.length();
}
// get the string for this entry only, since the entryStartOffset
// points to the '\n' befor the 'Name: ' we increase it by 1
// this is guaranteed to not go past end-of-string and be less
// then entryEndOffset.
entryStr = mfStr.substring(entryStartOffset + 1, entryEndOffset);
entryStr = stripContinuations(entryStr);
break;
}
if (entryStr != null) {
// process the entry to retrieve the digest algorith name
String digestLine = getDigestLine(entryStr, null);
// throw parsing
rtvValue = getMessageDigestName(digestLine);
}
return rtvValue;
}
/**
* @param mfBuf the data from an MF file of a JAR archive
*
* This method will populate the "digest type & result" hashtables
* with whatever entries it can correctly parse from the MF file and with the same digest algorithm.
* it will 'skip' incorrect entries (TODO: should the correct behavior
* be to throw an exception, or return an error code?)...
*
*
*/
private void populateManifest(byte mfBuf[], String digAlg) {
// need to make a string from the MF file data bytes
String mfStr = new String(mfBuf);
// start parsing each entry in the MF String
int entryStartOffset = mfStr.indexOf(MF_ENTRY_NEWLN_NAME);
int length = mfStr.length();
while ((entryStartOffset != -1) && (entryStartOffset < length)) {
// get the start of the next 'entry', i.e. the end of this entry
int entryEndOffset = mfStr.indexOf(MF_ENTRY_NEWLN_NAME, entryStartOffset + 1);
if (entryEndOffset == -1) {
// if there is no next entry, then the end of the string
// is the end of this entry
entryEndOffset = mfStr.length();
}
// get the string for this entry only, since the entryStartOffset
// points to the '\n' befor the 'Name: ' we increase it by 1
// this is guaranteed to not go past end-of-string and be less
// then entryEndOffset.
String entryStr = mfStr.substring(entryStartOffset + 1, entryEndOffset);
entryStr = stripContinuations(entryStr);
// entry points to the start of the next 'entry'
String entryName = getEntryFileName(entryStr);
// if we could retrieve an entry name, then we will extract
// digest type list, and the digest value list
if (entryName != null) {
String aDigestLine = getDigestLine(entryStr, digAlg);
if (aDigestLine != null) {
String msgDigestAlgorithm = getDigestAlgorithmFromString(aDigestLine);
byte digestResultsList[] = getDigestResultsList(aDigestLine);
//
// only insert this entry into the table if its
// "well-formed",
// i.e. only if we could extract its name, digest types, and
// digest-results
//
// sanity check, if the 2 lists are non-null, then their
// counts must match
//
// if ((msgDigestObj != null) && (digestResultsList != null)
// && (1 != digestResultsList.length)) {
// throw new RuntimeException(
// "Errors occurs when parsing the manifest file stream!"); //$NON-NLS-1$
// }
if (digests4entries == null) {
digests4entries = new Hashtable(10);
results4entries = new Hashtable(10);
}
// TODO throw exception if duplicate entry??
// no need, in theory, there is impossible to have two
// duplicate entries unless the manifest file
// is tampered
if (!digests4entries.contains(entryName)) {
digests4entries.put(entryName, msgDigestAlgorithm);
results4entries.put(entryName, digestResultsList);
}
} // could get lines of digest entries in this MF file entry
} // could retrieve entry name
// increment the offset to the ending entry...
entryStartOffset = entryEndOffset;
}
}
private String stripContinuations(String entry) {
if (entry.indexOf("\n ") < 0) //$NON-NLS-1$
return entry;
StringBuffer buffer = new StringBuffer(entry.length());
int cont = entry.indexOf("\n "); //$NON-NLS-1$
int start = 0;
while (cont >= 0) {
buffer.append(entry.substring(start, cont - 1));
start = cont + 2;
cont = cont + 2 < entry.length() ? entry.indexOf("\n ", cont + 2) : -1; //$NON-NLS-1$
}
// get the last one continuation
if (start < entry.length())
buffer.append(entry.substring(start));
return buffer.toString();
}
private String getEntryFileName(String manifestEntry) {
// get the beginning of the name
int nameStart = manifestEntry.indexOf(MF_ENTRY_NAME);
if (nameStart == -1) {
return null;
}
// check where the name ends
int nameEnd = manifestEntry.indexOf('\n', nameStart);
if (nameEnd == -1) {
return null;
}
// if there is a '\r' before the '\n', then we'll strip it
if (manifestEntry.charAt(nameEnd - 1) == '\r') {
nameEnd--;
}
// get to the beginning of the actual name...
nameStart += MF_ENTRY_NAME.length();
if (nameStart >= nameEnd) {
return null;
}
return manifestEntry.substring(nameStart, nameEnd);
}
/**
*
* @param manifestEntry contains a single MF file entry of the format
* "Name: foo"
* "MD5-Digest: [base64 encoded MD5 digest data]"
* "SHA1-Digest: [base64 encoded SHA1 digest dat]"
*
* @param manifestEntry a entry contains either one or multiple digest lines
* @param desireDigestAlg a string representing the desire digest value to be returned if there are
* multiple digest lines.
* If this value is null, return whatever digest value is in the entry.
*
* @return this function returns a digest line based on the desire digest algorithm value
* (since only MD5 and SHA1 are recognized here),
* or a 'null' will be returned if none of the digest algorithms
* were recognized.
*/
private String getDigestLine(String manifestEntry, String desireDigestAlg) {
String rtvValue = null;
// find the first digest line
int indexDigest = manifestEntry.indexOf(MF_DIGEST_PART);
// if we didn't find any digests at all, then we are done
if (indexDigest == -1)
return null;
// while we continue to find digest entries
// note: in the following loop we bail if any of the lines
// look malformed...
while (indexDigest != -1) {
// see where this digest line begins (look to left)
int indexStart = manifestEntry.lastIndexOf('\n', indexDigest);
if (indexStart == -1)
return null;
// see where it ends (look to right)
int indexEnd = manifestEntry.indexOf('\n', indexDigest);
if (indexEnd == -1)
return null;
// strip off ending '\r', if any
int indexEndToUse = indexEnd;
if (manifestEntry.charAt(indexEndToUse - 1) == '\r')
indexEndToUse--;
// indexStart points to the '\n' before this digest line
int indexStartToUse = indexStart + 1;
if (indexStartToUse >= indexEndToUse)
return null;
// now this may be a valid digest line, parse it a bit more
// to see if this is a preferred digest algorithm
String digestLine = manifestEntry.substring(indexStartToUse, indexEndToUse);
String digAlg = getMessageDigestName(digestLine);
if (desireDigestAlg != null && desireDigestAlg.equalsIgnoreCase(digAlg)) {
rtvValue = digestLine;
break;
}
// desireDigestAlg is null, always return the
rtvValue = digestLine;
// iterate to next digest line in this entry
indexDigest = manifestEntry.indexOf(MF_DIGEST_PART, indexEnd);
}
// if we couldn't find any digest lines, then we are done
return rtvValue;
}
private String getDigestAlgorithmFromString(String digestLines) {
if (digestLines != null) {
// String sDigestLine = digestLines[i];
int indexDigest = digestLines.indexOf(MF_DIGEST_PART);
String sDigestAlgType = digestLines.substring(0, indexDigest);
if (sDigestAlgType.equalsIgnoreCase(MD5_STR)) {
// remember the "algorithm type"
return MD5_STR;
} else if (sDigestAlgType.equalsIgnoreCase(SHA1_STR)) {
// remember the "algorithm type" object
return SHA1_STR;
} else {
// unknown algorithm type, we will stop processing this entry
// break;
throw new SecurityException(NLS.bind(JarVerifierMessages.Algorithm_Not_Supported, sDigestAlgType));
}
}
return null;
}
/**
* Return the Message Digest name
*
* @param digLine the message digest line is in the following format. That is in the
* following format:
* DIGEST_NAME-digest: digest value
* @return a string representing a message digest.
*/
private String getMessageDigestName(String digLine) {
String rtvValue = null;
if (digLine != null) {
int indexDigest = digLine.indexOf(MF_DIGEST_PART);
if (indexDigest != -1) {
rtvValue = digLine.substring(0, indexDigest);
}
}
return rtvValue;
}
private byte[] getDigestResultsList(String digestLines) {
byte resultsList[] = null;
if (digestLines != null) {
// for each digest-line retrieve the digest result
// for (int i = 0; i < digestLines.length; i++) {
String sDigestLine = digestLines;
int indexDigest = sDigestLine.indexOf(MF_DIGEST_PART);
indexDigest += MF_DIGEST_PART.length();
// if there is no data to extract for this digest value
// then we will fail...
if (indexDigest >= sDigestLine.length()) {
resultsList = null;
// break;
}
// now attempt to base64 decode the result
String sResult = sDigestLine.substring(indexDigest);
try {
resultsList = Base64.decode(sResult.getBytes());
} catch (Throwable t) {
// malformed digest result, no longer processing this entry
resultsList = null;
}
}
return resultsList;
}
static private int readFully(InputStream is, byte b[]) throws IOException {
int count = b.length;
int offset = 0;
int rc;
while ((rc = is.read(b, offset, count)) > 0) {
count -= rc;
offset += rc;
}
return offset;
}
byte[] readIntoArray(BundleEntry be) throws IOException {
int size = (int) be.getSize();
InputStream is = be.getInputStream();
byte b[] = new byte[size];
int rc = readFully(is, b);
if (rc != size) {
throw new IOException("Couldn't read all of " + be.getName() + ": " + rc + " != " + size); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
return b;
}
/**
* Sets the BundleFile for this singed bundle. It will extract
* signatures and digests from the bundle file and validate input streams
* before using them from the bundle file.
*
* @param bundleFile the BundleFile to extract elements from.
* @param the support flags for this signed bundle
* @throws IOException
*/
void setBundleFile(BundleFile bundleFile, int supportFlags) throws IOException {
this.bundleFile = bundleFile;
if (certsInitialized)
return;
BundleEntry be = bundleFile.getEntry(META_INF_MANIFEST_MF);
if (be == null)
return;
// read all the signature block file names into a list
Enumeration en = bundleFile.getEntryPaths(META_INF);
List signers = new ArrayList(2);
while (en.hasMoreElements()) {
String name = (String) en.nextElement();
if ((name.endsWith(DOT_DSA) || name.endsWith(DOT_RSA)) && name.indexOf('/') == name.lastIndexOf('/'))
signers.add(name);
}
// this means the jar is not signed
if (signers.size() == 0)
return;
byte manifestBytes[] = readIntoArray(be);
// determine the signer to be used for later
String latestSigner = findLatestSigner(bundleFile, signers);
// process the signers
try {
ArrayList processors = new ArrayList(signers.size());
Iterator iSigners = signers.iterator();
for (int i = 0; iSigners.hasNext(); i++) {
String signer = (String) iSigners.next();
PKCS7Processor processor = processSigner(bundleFile, manifestBytes, signer, latestSigner, supportFlags);
boolean error = false;
try {
processor.validateCerts();
determineCertsTrust(processor, supportFlags);
} catch (CertificateExpiredException e) {
// ignore
} catch (CertificateNotYetValidException e) {
// ignore
} catch (InvalidKeyException e) {
error = true;
}
if (!error) {
// make sure the latestSigner is the first in the list
if (signer == latestSigner)
processors.add(0, processor);
else
processors.add(processor);
}
}
chains = processors.size() == 0 ? null : (CertificateChain[]) processors.toArray(new CertificateChain[processors.size()]);
} catch (SignatureException e) {
throw new SecurityException(NLS.bind(JarVerifierMessages.Signature_Not_Verify_1, new String[] {latestSigner, bundleFile.toString()}));
}
}
private void determineCertsTrust(PKCS7Processor signerPKCS7, int supportFlags) {
CertificateTrustAuthority trustAuthority;
if ((supportFlags & SignedBundleHook.VERIFY_TRUST) != 0)
trustAuthority = SignedBundleHook.getTrustAuthority();
else
trustAuthority = trustAllAuthority;
if (trustAuthority != null)
signerPKCS7.determineTrust(trustAuthority);
}
private PKCS7Processor processSigner(BundleFile bf, byte[] manifestBytes, String signer, String latestSigner, int supportFlags) throws IOException, SignatureException {
BundleEntry be = bf.getEntry(signer);
byte pkcs7Bytes[] = readIntoArray(be);
int dotIndex = signer.lastIndexOf('.');
be = bf.getEntry(signer.substring(0, dotIndex) + DOT_SF);
byte sfBytes[] = readIntoArray(be);
// Step 1, verify the .SF file is signed by the private key that corresponds to the public key
// in the .RSA/.DSA file
PKCS7Processor chain = null;
try {
chain = new PKCS7Processor(pkcs7Bytes, 0, pkcs7Bytes.length);
// call the Step 1 in the Jar File Verification algorithm
chain.verifySFSignature(sfBytes, 0, sfBytes.length);
// algorithm used
String digAlg = getDigAlgFromSF(sfBytes);
if (digAlg == null) {
throw new SecurityException(NLS.bind(JarVerifierMessages.SF_File_Parsing_Error, new String[] {bf.toString()}));
}
// populate the two digest hashtable instance variable based on the used Message Digest algorithm
if (latestSigner == signer) { // only do this if it is the latest signer
// Process the Step 2 in the Jar File Verification algorithm
// Get the manifest out of the signature file and make sure
// it matches MANIFEST.MF
verifyManifestAndSingatureFile(manifestBytes, sfBytes);
// only populate the manifests if we are verifying content at runtime
if ((supportFlags & SignedBundleHook.VERIFY_RUNTIME) != 0)
populateManifest(manifestBytes, digAlg);
}
// process the Step 3
// read each file in the JAR file that has an entry in the .SF file.
// also determine the
// verifySFEntriesDigest(bf, sfBytes, manifestBytes);
} catch (InvalidKeyException e) {
SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
throw new SecurityException(NLS.bind(JarVerifierMessages.Invalid_Key_Exception, new String[] {bf.getBaseFile().toString(), e.getMessage()}));
} catch (CertificateException e) {
SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
throw new SecurityException(NLS.bind(JarVerifierMessages.PKCS7_Cert_Excep, new String[] {bf.getBaseFile().toString(), e.getMessage()}));
} catch (NoSuchAlgorithmException e) {
SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
throw new SecurityException(NLS.bind(JarVerifierMessages.PKCS7_No_Such_Algorithm, new String[] {bf.getBaseFile().toString(), e.getMessage()}));
}
return chain;
}
private String findLatestSigner(BundleFile bf, List names) {
String result = null;
long latestTime = Long.MIN_VALUE;
for (Iterator iNames = names.iterator(); iNames.hasNext();) {
String name = (String) iNames.next();
BundleEntry entry = bf.getEntry(name);
if (entry.getTime() > latestTime) {
result = name;
latestTime = entry.getTime();
}
}
return result;
}
/**
* Returns the Base64 encoded digest of the passed set of bytes.
*/
private String calculateDigest(MessageDigest digest, byte[] bytes) {
return new String(Base64.encode(digest.digest(bytes)));
}
public File getFile(String path, boolean nativeCode) {
return bundleFile.getFile(path, nativeCode);
}
public BundleEntry getEntry(String path) {
// strip off leading slashes so we can ensure the path matches the one provided in the manifest.
if (path.length() > 0 && path.charAt(0) == '/')
path = path.substring(1);
BundleEntry be = bundleFile.getEntry(path);
if (digests4entries == null)
return be;
if (be == null) {
if (digests4entries.get(path) == null)
return null;
throw new SecurityException(NLS.bind(JarVerifierMessages.file_is_removed_from_jar, getBaseFile().toString(), path));
}
if (be.getName().startsWith(META_INF))
return be;
if (!isSigned())
// If there is no signatures, we just return the regular bundle entry
return be;
return new SignedBundleEntry(be);
}
public Enumeration getEntryPaths(String path) {
return bundleFile.getEntryPaths(path);
}
public void close() throws IOException {
bundleFile.close();
}
public void open() throws IOException {
bundleFile.open();
}
public boolean containsDir(String dir) {
return bundleFile.containsDir(dir);
}
boolean matchDNChain(String pattern) {
CertificateChain[] matchChains = getChains();
for (int i = 0; i < matchChains.length; i++)
if (matchChains[i].isTrusted() && DNChainMatching.match(matchChains[i].getChain(), pattern))
return true;
return false;
}
public File getBaseFile() {
return bundleFile.getBaseFile();
}
class SignedBundleEntry extends BundleEntry {
BundleEntry nestedEntry;
SignedBundleEntry(BundleEntry nestedEntry) {
this.nestedEntry = nestedEntry;
}
public InputStream getInputStream() throws IOException {
String name = getName();
String digest = digests4entries == null ? null : (String) digests4entries.get(name);
if (digest == null)
// the digest does not exist; this must be a corrupted jar
throw new IOException("Corrupted file: the digest does not exist for the file " + name); //$NON-NLS-1$
byte results[] = (byte[]) results4entries.get(name);
// TODO ELI: it is probbaly best to decode the value of digest into Base64 base. This will only optimize the bad jar case.
return new DigestedInputStream(nestedEntry.getInputStream(), digest, results, nestedEntry.getSize());
}
public long getSize() {
return nestedEntry.getSize();
}
public String getName() {
return nestedEntry.getName();
}
public long getTime() {
return nestedEntry.getTime();
}
public URL getLocalURL() {
return nestedEntry.getLocalURL();
}
public URL getFileURL() {
return nestedEntry.getFileURL();
}
}
public void checkContent() throws CertificateException, CertificateExpiredException, SignatureException {
if (!isSigned() || digests4entries == null)
return;
for (Enumeration entries = digests4entries.keys(); entries.hasMoreElements();) {
String name = (String) entries.nextElement();
BundleEntry entry = getEntry(name);
if (entry == null) {
throw new SecurityException(NLS.bind(JarVerifierMessages.Jar_Is_Tampered, bundleFile.getBaseFile().getName()));
}
try {
entry.getBytes();
} catch (IOException e) {
SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
throw new SecurityException(NLS.bind(JarVerifierMessages.File_In_Jar_Is_Tampered, new String[] {name, bundleFile.getBaseFile().toString()}));
}
}
// validate the certs chain and determine the trust
for (int i = 0; i < chains.length; i++) {
PKCS7Processor signerPKCS7 = (PKCS7Processor) chains[i];
try {
signerPKCS7.validateCerts();
} catch (InvalidKeyException e) {
throw new CertificateException(e.getMessage());
}
// determine the trust of certificates
determineCertsTrust(signerPKCS7, SignedBundleHook.VERIFY_ALL);
}
}
public String[] verifyContent() {
if (!isSigned() || digests4entries == null)
return EMPTY_STRING;
ArrayList corrupted = new ArrayList(0); // be optimistic
for (Enumeration entries = digests4entries.keys(); entries.hasMoreElements();) {
String name = (String) entries.nextElement();
BundleEntry entry = getEntry(name);
if (entry == null)
corrupted.add(name); // we expected the entry to be here
else
try {
entry.getBytes();
} catch (IOException e) {
// must be invalid
corrupted.add(name);
}
}
return corrupted.size() == 0 ? EMPTY_STRING : (String[]) corrupted.toArray(new String[corrupted.size()]);
}
public CertificateChain[] getChains() {
if (!isSigned())
return new CertificateChain[0];
return chains;
}
public boolean isSigned() {
return chains != null;
}
static synchronized MessageDigest getMessageDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
}
return null;
}
// static public void main(String args[]) throws IOException {
//
// ZipBundleFile jf = new ZipBundleFile(new File(args[0]), null);
// SignedBundleFile sr = new SignedBundleFile();
//
// sr.setBundleFile(jf);
//
// // read the first level directory entries
// Enumeration en = sr.getEntryPaths("/"); //$NON-NLS-1$
// while (en.hasMoreElements()) {
// String filePath = (String) en.nextElement();
// System.out.println("main(): " + filePath); //$NON-NLS-1$
//
// // if this file is not a directory file
// // then we'll get its input stream for testing
// if (filePath.indexOf('/') == -1) {
// BundleEntry be = sr.getEntry(filePath);
// InputStream is = be.getInputStream();
// is.skip(be.getSize());
// is.read();
// is.close();
// }
// }
//
// if (!sr.isSigned()) {
// System.out.println("No signers present"); //$NON-NLS-1$
// } else {
// CertificateChain[] chains = sr.getChains();
// for (int i = 0; i < chains.length; i++) {
// System.out.println(chains[i].getChain());
// }
// }
//
// System.out.println("Done"); //$NON-NLS-1$
// }
}