| /******************************************************************************* |
| * Copyright (c) 2007, 2015 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.framework.log.FrameworkLogEntry; |
| import org.eclipse.osgi.signedcontent.SignerInfo; |
| import org.eclipse.osgi.storage.bundlefile.BundleEntry; |
| import org.eclipse.osgi.storage.bundlefile.BundleFile; |
| 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; |
| private final SignedBundleHook signedBundleHook; |
| |
| public SignatureBlockProcessor(SignedBundleFile signedContent, int supportFlags, SignedBundleHook signedBundleHook) { |
| this.signedBundle = signedContent; |
| this.supportFlags = supportFlags; |
| this.signedBundleHook = signedBundleHook; |
| } |
| |
| public SignedContentImpl process() throws IOException, InvalidKeyException, SignatureException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException { |
| BundleFile wrappedBundleFile = signedBundle.getBundleFile(); |
| 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, SignedContentConstants.UTF8); |
| 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) { |
| // need to make a string from the MF file data bytes |
| String mfStr = new String(mfBuf, SignedContentConstants.UTF8); |
| |
| // 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) { |
| 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)), SignedContentConstants.UTF8); |
| } |
| |
| 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, SignedContentConstants.UTF8); |
| 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; |
| } |
| } |