Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/signedcontent/SignatureBlockProcessor.java')
-rw-r--r--bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/signedcontent/SignatureBlockProcessor.java499
1 files changed, 499 insertions, 0 deletions
diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/signedcontent/SignatureBlockProcessor.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/signedcontent/SignatureBlockProcessor.java
new file mode 100644
index 000000000..8f9684147
--- /dev/null
+++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/signedcontent/SignatureBlockProcessor.java
@@ -0,0 +1,499 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 2012 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.signedcontent;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+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.signedcontent.SignerInfo;
+import org.eclipse.osgi.util.NLS;
+
+public class SignatureBlockProcessor implements SignedContentConstants {
+ private final SignedBundleFile signedBundle;
+ private List<SignerInfo> signerInfos = new ArrayList<SignerInfo>();
+ private Map<String, Object> contentMDResults = new HashMap<String, Object>();
+ // map of tsa singers keyed by SignerInfo -> {tsa_SignerInfo, signingTime}
+ private Map<SignerInfo, Object[]> tsaSignerInfos;
+ private final int supportFlags;
+
+ public SignatureBlockProcessor(SignedBundleFile signedContent, int supportFlags) {
+ this.signedBundle = signedContent;
+ this.supportFlags = supportFlags;
+ }
+
+ public SignedContentImpl process() throws IOException, InvalidKeyException, SignatureException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException {
+ BundleFile wrappedBundleFile = signedBundle.getWrappedBundleFile();
+ BundleEntry be = wrappedBundleFile.getEntry(META_INF_MANIFEST_MF);
+ if (be == null)
+ return createUnsignedContent();
+
+ // read all the signature block file names into a list
+ Enumeration<String> en = wrappedBundleFile.getEntryPaths(META_INF);
+ List<String> signers = new ArrayList<String>(2);
+ while (en.hasMoreElements()) {
+ String name = 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 createUnsignedContent();
+
+ byte manifestBytes[] = readIntoArray(be);
+ // process the signers
+
+ for (Iterator<String> iSigners = signers.iterator(); iSigners.hasNext();)
+ processSigner(wrappedBundleFile, manifestBytes, iSigners.next());
+
+ // done processing now create a SingedContent to return
+ SignerInfo[] allSigners = signerInfos.toArray(new SignerInfo[signerInfos.size()]);
+ for (Iterator<Map.Entry<String, Object>> iResults = contentMDResults.entrySet().iterator(); iResults.hasNext();) {
+ Map.Entry<String, Object> entry = iResults.next();
+ @SuppressWarnings("unchecked")
+ List<Object>[] value = (List<Object>[]) entry.getValue();
+ SignerInfo[] entrySigners = value[0].toArray(new SignerInfo[value[0].size()]);
+ byte[][] entryResults = value[1].toArray(new byte[value[1].size()][]);
+ entry.setValue(new Object[] {entrySigners, entryResults});
+ }
+ SignedContentImpl result = new SignedContentImpl(allSigners, (supportFlags & SignedBundleHook.VERIFY_RUNTIME) != 0 ? contentMDResults : null);
+ result.setContent(signedBundle);
+ result.setTSASignerInfos(tsaSignerInfos);
+ return result;
+ }
+
+ private SignedContentImpl createUnsignedContent() {
+ SignedContentImpl result = new SignedContentImpl(new SignerInfo[0], contentMDResults);
+ result.setContent(signedBundle);
+ return result;
+ }
+
+ private void processSigner(BundleFile bf, byte[] manifestBytes, String signer) throws IOException, SignatureException, InvalidKeyException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException {
+ 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
+ String baseFile = bf.getBaseFile() != null ? bf.getBaseFile().toString() : null;
+ PKCS7Processor processor = new PKCS7Processor(pkcs7Bytes, 0, pkcs7Bytes.length, signer, baseFile);
+ // call the Step 1 in the Jar File Verification algorithm
+ processor.verifySFSignature(sfBytes, 0, sfBytes.length);
+ // algorithm used
+ String digAlg = getDigAlgFromSF(sfBytes);
+ if (digAlg == null)
+ throw new SignatureException(NLS.bind(SignedContentMessages.SF_File_Parsing_Error, new String[] {bf.toString()}));
+ // get the digest results
+ // 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
+ verifyManifestAndSignatureFile(manifestBytes, sfBytes);
+
+ // create a SignerInfo with the processed information
+ SignerInfoImpl signerInfo = new SignerInfoImpl(processor.getCertificates(), null, digAlg);
+ if ((supportFlags & SignedBundleHook.VERIFY_RUNTIME) != 0)
+ // only populate the manifests digest information for verifying content at runtime
+ populateMDResults(manifestBytes, signerInfo);
+ signerInfos.add(signerInfo);
+ // check for tsa signers
+ Certificate[] tsaCerts = processor.getTSACertificates();
+ Date signingTime = processor.getSigningTime();
+ if (tsaCerts != null && signingTime != null) {
+ SignerInfoImpl tsaSignerInfo = new SignerInfoImpl(tsaCerts, null, digAlg);
+ if (tsaSignerInfos == null)
+ tsaSignerInfos = new HashMap<SignerInfo, Object[]>(2);
+ tsaSignerInfos.put(signerInfo, new Object[] {tsaSignerInfo, signingTime});
+ }
+ }
+
+ /**
+ * Verify the digest listed in each entry in the .SF file with corresponding section in the manifest
+ * @throws SignatureException
+ */
+ private void verifyManifestAndSignatureFile(byte[] manifestBytes, byte[] sfBytes) throws SignatureException {
+
+ 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 manifestDigest = 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))
+ manifestDigest = calculateDigest(getMessageDigest(MD5_STR), manifestBytes);
+ else if (digestName.equalsIgnoreCase(SHA1_STR))
+ manifestDigest = calculateDigest(getMessageDigest(SHA1_STR), manifestBytes);
+ else
+ manifestDigest = calculateDigest(getMessageDigest(digestName), manifestBytes);
+ 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 (!digestValue.equals(manifestDigest)) {
+ SignatureException se = new SignatureException(NLS.bind(SignedContentMessages.Security_File_Is_Tampered, new String[] {signedBundle.getBaseFile().toString()}));
+ SignedBundleHook.log(se.getMessage(), FrameworkLogEntry.ERROR, se);
+ throw se;
+ }
+ }
+ }
+ }
+
+ private void populateMDResults(byte mfBuf[], SignerInfo signerInfo) throws NoSuchAlgorithmException {
+ // 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, signerInfo.getMessageDigestAlgorithm());
+
+ if (aDigestLine != null) {
+ String msgDigestAlgorithm = getDigestAlgorithmFromString(aDigestLine);
+ if (!msgDigestAlgorithm.equalsIgnoreCase(signerInfo.getMessageDigestAlgorithm()))
+ continue; // TODO log error?
+ byte digestResult[] = 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$
+ // }
+ @SuppressWarnings("unchecked")
+ List<Object>[] mdResult = (List<Object>[]) contentMDResults.get(entryName);
+ if (mdResult == null) {
+ @SuppressWarnings("unchecked")
+ List<Object>[] arrayLists = new ArrayList[2];
+ mdResult = arrayLists;
+ mdResult[0] = new ArrayList<Object>();
+ mdResult[1] = new ArrayList<Object>();
+ contentMDResults.put(entryName, mdResult);
+ }
+ mdResult[0].add(signerInfo);
+ mdResult[1].add(digestResult);
+ } // 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 static 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;
+ }
+
+ private static String getDigestAlgorithmFromString(String digestLines) throws NoSuchAlgorithmException {
+ 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 {
+ return sDigestAlgType;
+ }
+ }
+ return null;
+ }
+
+ private static 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);
+ }
+
+ /**
+ * Returns the Base64 encoded digest of the passed set of bytes.
+ */
+ private static String calculateDigest(MessageDigest digest, byte[] bytes) {
+ return new String(Base64.encode(digest.digest(bytes)));
+ }
+
+ static synchronized MessageDigest getMessageDigest(String algorithm) {
+ try {
+ return MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
+ }
+ return null;
+ }
+
+ /**
+ * 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 static String getDigAlgFromSF(byte SFBuf[]) {
+ // 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
+ return getMessageDigestName(digestLine);
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @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 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 static String getDigestLine(String manifestEntry, String desireDigestAlg) {
+ String result = 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) {
+ if (desireDigestAlg.equalsIgnoreCase(digAlg))
+ return digestLine;
+ }
+
+ // desireDigestAlg is null, always return the digestLine
+ result = 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 result;
+ }
+
+ /**
+ * 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 static 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 static String stripContinuations(String entry) {
+ if (entry.indexOf("\n ") < 0 && entry.indexOf("\r ") < 0) //$NON-NLS-1$//$NON-NLS-2$
+ return entry;
+ StringBuffer buffer = new StringBuffer(entry);
+ removeAll(buffer, "\r\n "); //$NON-NLS-1$
+ removeAll(buffer, "\n "); //$NON-NLS-1$
+ removeAll(buffer, "\r "); //$NON-NLS-1$
+ return buffer.toString();
+ }
+
+ private static StringBuffer removeAll(StringBuffer buffer, String toRemove) {
+ int index = buffer.indexOf(toRemove);
+ int length = toRemove.length();
+ while (index > 0) {
+ buffer.replace(index, index + length, ""); //$NON-NLS-1$
+ index = buffer.indexOf(toRemove, index);
+ }
+ return buffer;
+ }
+
+ private static byte[] readIntoArray(BundleEntry be) throws IOException {
+ int size = (int) be.getSize();
+ InputStream is = be.getInputStream();
+ try {
+ 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;
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // do nothing;
+ }
+ }
+ }
+
+ private static 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;
+ }
+}

Back to the top