001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package java.util.jar;
019:
020: import java.io.ByteArrayInputStream;
021: import java.io.IOException;
022: import java.io.OutputStream;
023: import java.io.UnsupportedEncodingException;
024: import java.security.GeneralSecurityException;
025: import java.security.MessageDigest;
026: import java.security.NoSuchAlgorithmException;
027: import java.security.cert.Certificate;
028: import java.util.HashMap;
029: import java.util.Hashtable;
030: import java.util.Iterator;
031: import java.util.Map;
032: import java.util.StringTokenizer;
033: import java.util.Vector;
034: import java.util.zip.ZipEntry;
035:
036: import org.apache.harmony.archive.internal.nls.Messages;
037: import org.apache.harmony.luni.util.Base64;
038: import org.apache.harmony.security.utils.JarUtils;
039:
040: import org.apache.harmony.archive.util.Util;
041:
042: /**
043: * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage
044: * the verification of signed jars. <code>JarFile</code> and
045: * <code>JarInputStream</code> objects will be expected to have a
046: * <code>JarVerifier</code> instance member which can be used to carry out the
047: * tasks associated with verifying a signed jar. These tasks would typically
048: * include:
049: * <ul>
050: * <li>verification of all signed signature files
051: * <li>confirmation that all signed data was signed only by the party or
052: * parties specified in the signature block data
053: * <li>verification that the contents of all signature files (i.e.
054: * <code>.SF</code> files) agree with the jar entries information found in the
055: * jar manifest.
056: * </ul>
057: */
058: class JarVerifier {
059:
060: private final String jarName;
061:
062: private Manifest man;
063:
064: private HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(
065: 5);
066:
067: private final Hashtable<String, HashMap<String, Attributes>> signatures = new Hashtable<String, HashMap<String, Attributes>>(
068: 5);
069:
070: private final Hashtable<String, Certificate[]> certificates = new Hashtable<String, Certificate[]>(
071: 5);
072:
073: private final Hashtable<String, Certificate[]> verifiedEntries = new Hashtable<String, Certificate[]>();
074:
075: byte[] mainAttributesChunk;
076:
077: /**
078: * TODO Type description
079: */
080: static class VerifierEntry extends OutputStream {
081:
082: MessageDigest digest;
083:
084: byte[] hash;
085:
086: Certificate[] certificates;
087:
088: VerifierEntry(MessageDigest digest, byte[] hash,
089: Certificate[] certificates) {
090: this .digest = digest;
091: this .hash = hash;
092: this .certificates = certificates;
093: }
094:
095: /*
096: * (non-Javadoc)
097: *
098: * @see java.io.OutputStream#write(int)
099: */
100: @Override
101: public void write(int value) {
102: digest.update((byte) value);
103: }
104:
105: /*
106: * (non-Javadoc)
107: *
108: * @see java.io.OutputStream#write(byte[], int, int)
109: */
110: @Override
111: public void write(byte[] buf, int off, int nbytes) {
112: digest.update(buf, off, nbytes);
113: }
114: }
115:
116: /**
117: * Constructs and answers with a new instance of JarVerifier.
118: *
119: * @param name
120: * the name of the jar file being verified.
121: */
122: JarVerifier(String name) {
123: jarName = name;
124: }
125:
126: /**
127: * Called for each new jar entry read in from the input stream. This method
128: * constructs and returns a new {@link VerifierEntry} which contains the
129: * certificates used to sign the entry and its hash value as specified in
130: * the jar manifest.
131: *
132: * @param name
133: * the name of an entry in a jar file which is <b>not</b> in the
134: * <code>META-INF</code> directory.
135: * @return a new instance of {@link VerifierEntry} which can be used by
136: * callers as an {@link OutputStream}.
137: */
138: VerifierEntry initEntry(String name) {
139: // If no manifest is present by the time an entry is found,
140: // verification cannot occur. If no signature files have
141: // been found, do not verify.
142: if (man == null || signatures.size() == 0) {
143: return null;
144: }
145:
146: Attributes attributes = man.getAttributes(name);
147: // entry has no digest
148: if (attributes == null) {
149: return null;
150: }
151:
152: Vector<Certificate> certs = new Vector<Certificate>();
153: Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures
154: .entrySet().iterator();
155: while (it.hasNext()) {
156: Map.Entry<String, HashMap<String, Attributes>> entry = it
157: .next();
158: HashMap<String, Attributes> hm = entry.getValue();
159: if (hm.get(name) != null) {
160: // Found an entry for entry name in .SF file
161: String signatureFile = entry.getKey();
162:
163: Vector<Certificate> newCerts = getSignerCertificates(
164: signatureFile, certificates);
165: Iterator<Certificate> iter = newCerts.iterator();
166: while (iter.hasNext()) {
167: certs.add(iter.next());
168: }
169: }
170: }
171:
172: // entry is not signed
173: if (certs.size() == 0) {
174: return null;
175: }
176: Certificate[] certificatesArray = new Certificate[certs.size()];
177: certs.toArray(certificatesArray);
178:
179: String algorithms = attributes.getValue("Digest-Algorithms"); //$NON-NLS-1$
180: if (algorithms == null) {
181: algorithms = "SHA SHA1"; //$NON-NLS-1$
182: }
183: StringTokenizer tokens = new StringTokenizer(algorithms);
184: while (tokens.hasMoreTokens()) {
185: String algorithm = tokens.nextToken();
186: String hash = attributes.getValue(algorithm + "-Digest"); //$NON-NLS-1$
187: if (hash == null) {
188: continue;
189: }
190: byte[] hashBytes;
191: try {
192: hashBytes = hash.getBytes("ISO8859_1"); //$NON-NLS-1$
193: } catch (UnsupportedEncodingException e) {
194: throw new RuntimeException(e.toString());
195: }
196:
197: try {
198: return new VerifierEntry(MessageDigest
199: .getInstance(algorithm), hashBytes,
200: certificatesArray);
201: } catch (NoSuchAlgorithmException e) {
202: // Ignored
203: }
204: }
205: return null;
206: }
207:
208: /**
209: * Add a new meta entry to the internal collection of data held on each jar
210: * entry in the <code>META-INF</code> directory including the manifest
211: * file itself. Files associated with the signing of a jar would also be
212: * added to this collection.
213: *
214: * @param name
215: * the name of the file located in the <code>META-INF</code>
216: * directory.
217: * @param buf
218: * the file bytes for the file called <code>name</code>.
219: * @see #removeMetaEntries()
220: */
221: void addMetaEntry(String name, byte[] buf) {
222: metaEntries.put(Util.toASCIIUpperCase(name), buf);
223: }
224:
225: /**
226: * If the associated jar file is signed, check on the validity of all of the
227: * known signatures.
228: *
229: * @return <code>true</code> if the associated jar is signed and an
230: * internal check verifies the validity of the signature(s).
231: * <code>false</code> if the associated jar file has no entries at
232: * all in its <code>META-INF</code> directory. This situation is
233: * indicative of an invalid jar file.
234: * <p>
235: * Will also return true if the jar file is <i>not</i> signed.
236: * </p>
237: * @throws SecurityException
238: * if the jar file is signed and it is determined that a
239: * signature block file contains an invalid signature for the
240: * corresponding signature file.
241: */
242: synchronized boolean readCertificates() {
243: if (metaEntries == null) {
244: return false;
245: }
246: Iterator<String> it = metaEntries.keySet().iterator();
247: while (it.hasNext()) {
248: String key = it.next();
249: if (key.endsWith(".DSA") || key.endsWith(".RSA")) { //$NON-NLS-1$ //$NON-NLS-2$
250: verifyCertificate(key);
251: // Check for recursive class load
252: if (metaEntries == null) {
253: return false;
254: }
255: it.remove();
256: }
257: }
258: return true;
259: }
260:
261: /**
262: * @param certFile
263: */
264: private void verifyCertificate(String certFile) {
265: // Found Digital Sig, .SF should already have been read
266: String signatureFile = certFile.substring(0, certFile
267: .lastIndexOf('.'))
268: + ".SF"; //$NON-NLS-1$
269: byte[] sfBytes = metaEntries.get(signatureFile);
270: if (sfBytes == null) {
271: return;
272: }
273:
274: byte[] sBlockBytes = metaEntries.get(certFile);
275: try {
276: Certificate[] signerCertChain = JarUtils.verifySignature(
277: new ByteArrayInputStream(sfBytes),
278: new ByteArrayInputStream(sBlockBytes));
279: /*
280: * Recursive call in loading security provider related class which
281: * is in a signed jar.
282: */
283: if (null == metaEntries) {
284: return;
285: }
286: if (signerCertChain != null) {
287: certificates.put(signatureFile, signerCertChain);
288: }
289: } catch (IOException e) {
290: return;
291: } catch (GeneralSecurityException e) {
292: /* [MSG "archive.30", "{0} failed verification of {1}"] */
293: throw new SecurityException(Messages.getString(
294: "archive.30", jarName, signatureFile)); //$NON-NLS-1$
295: }
296:
297: // Verify manifest hash in .sf file
298: Attributes attributes = new Attributes();
299: HashMap<String, Attributes> hm = new HashMap<String, Attributes>();
300: try {
301: new InitManifest(new ByteArrayInputStream(sfBytes),
302: attributes, hm, null, "Signature-Version"); //$NON-NLS-1$
303: } catch (IOException e) {
304: return;
305: }
306:
307: boolean createdBySigntool = false;
308: String createdByValue = attributes.getValue("Created-By"); //$NON-NLS-1$
309: if (createdByValue != null) {
310: createdBySigntool = createdByValue.indexOf("signtool") != -1; //$NON-NLS-1$
311: }
312:
313: // Use .SF to verify the mainAttributes of the manifest
314: // If there is no -Digest-Manifest-Main-Attributes entry in .SF
315: // file, such as those created before java 1.5, then we ignore
316: // such verification.
317: // FIXME: The meaning of createdBySigntool
318: if (mainAttributesChunk != null && !createdBySigntool) {
319: String digestAttribute = "-Digest-Manifest-Main-Attributes"; //$NON-NLS-1$
320: if (!verify(attributes, digestAttribute,
321: mainAttributesChunk, false, true)) {
322: /* [MSG "archive.30", "{0} failed verification of {1}"] */
323: throw new SecurityException(Messages.getString(
324: "archive.30", jarName, signatureFile)); //$NON-NLS-1$
325: }
326: }
327:
328: byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
329: if (manifest == null) {
330: return;
331: }
332: // Use .SF to verify the whole manifest
333: String digestAttribute = createdBySigntool ? "-Digest" //$NON-NLS-1$
334: : "-Digest-Manifest"; //$NON-NLS-1$
335: if (!verify(attributes, digestAttribute, manifest, false, false)) {
336: Iterator<Map.Entry<String, Attributes>> it = hm.entrySet()
337: .iterator();
338: while (it.hasNext()) {
339: Map.Entry<String, Attributes> entry = it.next();
340: byte[] chunk = man.getChunk(entry.getKey());
341: if (chunk == null) {
342: return;
343: }
344: if (!verify(entry.getValue(), "-Digest", chunk, //$NON-NLS-1$
345: createdBySigntool, false)) {
346: /*
347: * [MSG "archive.31", "{0} has invalid digest for {1} in
348: * {2}"]
349: */
350: throw new SecurityException(Messages.getString(
351: "archive.31", //$NON-NLS-1$
352: new Object[] { signatureFile,
353: entry.getKey(), jarName }));
354: }
355: }
356: }
357: metaEntries.put(signatureFile, null);
358: signatures.put(signatureFile, hm);
359: }
360:
361: /**
362: * Associate this verifier with the specified {@link Manifest} object.
363: *
364: * @param mf
365: * a <code>java.util.jar.Manifest</code> object.
366: */
367: void setManifest(Manifest mf) {
368: man = mf;
369: }
370:
371: /**
372: * Verifies that the digests stored in the manifest match the decrypted
373: * digests from the .SF file. This indicates the validity of the signing,
374: * not the integrity of the file, as it's digest must be calculated and
375: * verified when its contents are read.
376: *
377: * @param entry
378: * the {@link VerifierEntry} associated with the specified
379: * <code>zipEntry</code>.
380: * @param zipEntry
381: * an entry in the jar file
382: * @throws SecurityException
383: * if the digest value stored in the manifest does <i>not</i>
384: * agree with the decrypted digest as recovered from the
385: * <code>.SF</code> file.
386: * @see #initEntry(String)
387: */
388: void verifySignatures(VerifierEntry entry, ZipEntry zipEntry) {
389: byte[] digest = entry.digest.digest();
390: if (!MessageDigest.isEqual(digest, Base64.decode(entry.hash))) {
391: /* [MSG "archive.31", "{0} has invalid digest for {1} in {2}"] */
392: throw new SecurityException(Messages
393: .getString("archive.31", new Object[] { //$NON-NLS-1$
394: JarFile.MANIFEST_NAME, zipEntry.getName(),
395: jarName }));
396: }
397: verifiedEntries.put(zipEntry.getName(), entry.certificates);
398: }
399:
400: /**
401: * Returns a <code>boolean</code> indication of whether or not the
402: * associated jar file is signed.
403: *
404: * @return <code>true</code> if the jar is signed, <code>false</code>
405: * otherwise.
406: */
407: boolean isSignedJar() {
408: return certificates.size() > 0;
409: }
410:
411: private boolean verify(Attributes attributes, String entry,
412: byte[] data, boolean ignoreSecondEndline, boolean ignorable) {
413: String algorithms = attributes.getValue("Digest-Algorithms"); //$NON-NLS-1$
414: if (algorithms == null) {
415: algorithms = "SHA SHA1"; //$NON-NLS-1$
416: }
417: StringTokenizer tokens = new StringTokenizer(algorithms);
418: while (tokens.hasMoreTokens()) {
419: String algorithm = tokens.nextToken();
420: String hash = attributes.getValue(algorithm + entry);
421: if (hash == null) {
422: continue;
423: }
424:
425: MessageDigest md;
426: try {
427: md = MessageDigest.getInstance(algorithm);
428: } catch (NoSuchAlgorithmException e) {
429: continue;
430: }
431: if (ignoreSecondEndline && data[data.length - 1] == '\n'
432: && data[data.length - 2] == '\n') {
433: md.update(data, 0, data.length - 1);
434: } else {
435: md.update(data, 0, data.length);
436: }
437: byte[] b = md.digest();
438: byte[] hashBytes;
439: try {
440: hashBytes = hash.getBytes("ISO8859_1"); //$NON-NLS-1$
441: } catch (UnsupportedEncodingException e) {
442: throw new RuntimeException(e.toString());
443: }
444: return MessageDigest.isEqual(b, Base64.decode(hashBytes));
445: }
446: return ignorable;
447: }
448:
449: /**
450: * Returns all of the {@link java.security.cert.Certificate} instances that
451: * were used to verify the signature on the jar entry called
452: * <code>name</code>.
453: *
454: * @param name
455: * the name of a jar entry.
456: * @return an array of {@link java.security.cert.Certificate}.
457: */
458: Certificate[] getCertificates(String name) {
459: Certificate[] verifiedCerts = verifiedEntries.get(name);
460: if (verifiedCerts == null) {
461: return null;
462: }
463: return verifiedCerts.clone();
464: }
465:
466: /**
467: * Remove all entries from the internal collection of data held about each
468: * jar entry in the <code>META-INF</code> directory.
469: *
470: * @see #addMetaEntry(String, byte[])
471: */
472: void removeMetaEntries() {
473: metaEntries = null;
474: }
475:
476: /**
477: * Returns a <code>Vector</code> of all of the
478: * {@link java.security.cert.Certificate}s that are associated with the
479: * signing of the named signature file.
480: *
481: * @param signatureFileName
482: * the name of a signature file
483: * @param certificates
484: * a <code>Map</code> of all of the certificate chains
485: * discovered so far while attempting to verify the jar that
486: * contains the signature file <code>signatureFileName</code>.
487: * This object will have been previously set in the course of one
488: * or more calls to
489: * {@link #verifyJarSignatureFile(String, String, String, Map, Map)}
490: * where it was passed in as the last argument.
491: * @return all of the <code>Certificate</code> entries for the signer of
492: * the jar whose actions led to the creation of the named signature
493: * file.
494: */
495: public static Vector<Certificate> getSignerCertificates(
496: String signatureFileName,
497: Map<String, Certificate[]> certificates) {
498: Vector<Certificate> result = new Vector<Certificate>();
499: Certificate[] certChain = certificates.get(signatureFileName);
500: if (certChain != null) {
501: for (Certificate element : certChain) {
502: result.add(element);
503: }
504: }
505: return result;
506: }
507: }
|