Copyright (c) 2007, 2016 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation
/******************************************************************************* * Copyright (c) 2007, 2016 IBM Corporation and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which accompanies this distribution, * and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * 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<>(); private Map<String, Object> contentMDResults = new HashMap<>(); // 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<>(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 (Map.Entry<String, Object> entry : contentMDResults.entrySet()) { @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<>(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 –
/** * 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' before 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); // increment the offset to the ending entry for the next iteration of the loop ... entryStartOffset = entryEndOffset; // 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<>(); mdResult[1] = new ArrayList<>(); 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 } } 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.
/** * 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.
Params:
  • SFBuf – a .SF file in bytes
Returns: the digest algorithm name used in the .SF file
/** * 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(); if ((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' before 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); } if (entryStr != null) { // process the entry to retrieve the digest algorith name String digestLine = getDigestLine(entryStr, null); // throw parsing return getMessageDigestName(digestLine); } return null; }
Params:
  • 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]"
  • 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.
Returns: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.
/** * * @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
Params:
  • digLine – the message digest line is in the following format. That is in the following format: DIGEST_NAME-digest: digest value
Returns: a string representing a message digest.
/** * 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; StringBuilder buffer = new StringBuilder(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 StringBuilder removeAll(StringBuilder 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; } }