changeset 56292:94691d8e746f

8217375: jarsigner breaks old signature with long lines in manifest Reviewed-by: jjiang, weijun Contributed-by: Philipp Kunz <philipp.kunz@paratix.ch>
author weijun
date Thu, 18 Jul 2019 08:53:06 +0800
parents 643978a35f6e
children 388c36110e88
files src/java.base/share/classes/sun/security/util/ManifestDigester.java src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java src/jdk.jartool/share/classes/sun/tools/jar/Main.java test/jdk/ProblemList.txt test/jdk/sun/security/tools/jarsigner/DiffEnd.java test/jdk/sun/security/tools/jarsigner/DigestDontIgnoreCase.java test/jdk/sun/security/tools/jarsigner/EmptyIndividualSectionName.java test/jdk/sun/security/tools/jarsigner/EmptyJar.java test/jdk/sun/security/tools/jarsigner/FindHeaderEndVsManifestDigesterFindFirstSection.java test/jdk/sun/security/tools/jarsigner/InsufficientSectionDelimiter.java test/jdk/sun/security/tools/jarsigner/MainAttributesConfused.java test/jdk/sun/security/tools/jarsigner/OldSig.java test/jdk/sun/security/tools/jarsigner/PreserveRawManifestEntryAndDigest.java test/jdk/sun/security/tools/jarsigner/RemoveDifferentKeyAlgBlockFile.java test/jdk/sun/security/tools/jarsigner/SectionNameContinuedVsLineBreak.java test/jdk/sun/security/tools/jarsigner/Utils.java test/jdk/sun/security/tools/jarsigner/WasSignedByOtherSigner.java test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java test/jdk/sun/security/tools/jarsigner/compatibility/DetailsOutputStream.java test/jdk/sun/security/tools/jarsigner/compatibility/HtmlHelper.java test/jdk/sun/security/tools/jarsigner/compatibility/JdkUtils.java test/jdk/sun/security/tools/jarsigner/compatibility/README test/jdk/sun/security/tools/jarsigner/compatibility/SignTwice.java test/jdk/sun/security/tools/jarsigner/warnings/Test.java test/jdk/sun/security/util/ManifestDigester/DigestInput.java test/jdk/sun/security/util/ManifestDigester/FindSection.java test/jdk/sun/security/util/ManifestDigester/FindSections.java test/jdk/sun/security/util/ManifestDigester/LineBreaks.java test/jdk/sun/security/util/ManifestDigester/ReproduceRaw.java test/lib/jdk/test/lib/process/OutputAnalyzer.java
diffstat 31 files changed, 5473 insertions(+), 602 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/sun/security/util/ManifestDigester.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/src/java.base/share/classes/sun/security/util/ManifestDigester.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,10 +25,12 @@
 
 package sun.security.util;
 
-import java.security.*;
+import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.io.IOException;
 import java.util.List;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -40,13 +42,27 @@
  */
 public class ManifestDigester {
 
+    /**
+     * The part "{@code Manifest-Main-Attributes}" of the main attributes
+     * digest header name in a signature file as described in the jar
+     * specification:
+     * <blockquote>{@code x-Digest-Manifest-Main-Attributes}
+     * (where x is the standard name of a {@link MessageDigest} algorithm):
+     * The value of this attribute is the digest value of the main attributes
+     * of the manifest.</blockquote>
+     * @see <a href="{@docRoot}/../specs/jar/jar.html#signature-file">
+     * JAR File Specification, section Signature File</a>
+     * @see #getMainAttsEntry
+     */
     public static final String MF_MAIN_ATTRS = "Manifest-Main-Attributes";
 
     /** the raw bytes of the manifest */
-    private byte[] rawBytes;
+    private final byte[] rawBytes;
 
-    /** the entries grouped by names */
-    private HashMap<String, Entry> entries; // key is a UTF-8 string
+    private final Entry mainAttsEntry;
+
+    /** individual sections by their names */
+    private final HashMap<String, Entry> entries = new HashMap<>();
 
     /** state returned by findSection */
     static class Position {
@@ -72,29 +88,31 @@
     private boolean findSection(int offset, Position pos)
     {
         int i = offset, len = rawBytes.length;
-        int last = offset;
+        int last = offset - 1;
         int next;
         boolean allBlank = true;
 
-        pos.endOfFirstLine = -1;
+        /* denotes that a position is not yet assigned.
+         * As a primitive type int it cannot be null
+         * and -1 would be confused with (i - 1) when i == 0 */
+        final int UNASSIGNED = Integer.MIN_VALUE;
+
+        pos.endOfFirstLine = UNASSIGNED;
 
         while (i < len) {
             byte b = rawBytes[i];
             switch(b) {
             case '\r':
-                if (pos.endOfFirstLine == -1)
+                if (pos.endOfFirstLine == UNASSIGNED)
                     pos.endOfFirstLine = i-1;
-                if ((i < len) &&  (rawBytes[i+1] == '\n'))
+                if (i < len - 1 && rawBytes[i + 1] == '\n')
                     i++;
                 /* fall through */
             case '\n':
-                if (pos.endOfFirstLine == -1)
+                if (pos.endOfFirstLine == UNASSIGNED)
                     pos.endOfFirstLine = i-1;
                 if (allBlank || (i == len-1)) {
-                    if (i == len-1)
-                        pos.endOfSection = i;
-                    else
-                        pos.endOfSection = last;
+                    pos.endOfSection = allBlank ? last : i;
                     pos.startOfNext = i+1;
                     return true;
                 }
@@ -116,16 +134,17 @@
     public ManifestDigester(byte[] bytes)
     {
         rawBytes = bytes;
-        entries = new HashMap<>();
 
         Position pos = new Position();
 
-        if (!findSection(0, pos))
+        if (!findSection(0, pos)) {
+            mainAttsEntry = null;
             return; // XXX: exception?
+        }
 
         // create an entry for main attributes
-        entries.put(MF_MAIN_ATTRS, new Entry().addSection(
-                new Section(0, pos.endOfSection + 1, pos.startOfNext, rawBytes)));
+        mainAttsEntry = new Entry().addSection(new Section(
+                0, pos.endOfSection + 1, pos.startOfNext, rawBytes));
 
         int start = pos.startOfNext;
         while(findSection(start, pos)) {
@@ -133,14 +152,16 @@
             int sectionLen = pos.endOfSection-start+1;
             int sectionLenWithBlank = pos.startOfNext-start;
 
-            if (len > 6) {
+            if (len >= 6) { // 6 == "Name: ".length()
                 if (isNameAttr(bytes, start)) {
                     ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
                     nameBuf.write(bytes, start+6, len-6);
 
                     int i = start + len;
                     if ((i-start) < sectionLen) {
-                        if (bytes[i] == '\r') {
+                        if (bytes[i] == '\r'
+                                && i + 1 - start < sectionLen
+                                && bytes[i + 1] == '\n') {
                             i += 2;
                         } else {
                             i += 1;
@@ -152,14 +173,16 @@
                             // name is wrapped
                             int wrapStart = i;
                             while (((i-start) < sectionLen)
-                                    && (bytes[i++] != '\n'));
-                            if (bytes[i-1] != '\n')
-                                return; // XXX: exception?
-                            int wrapLen;
-                            if (bytes[i-2] == '\r')
-                                wrapLen = i-wrapStart-2;
-                            else
-                                wrapLen = i-wrapStart-1;
+                                    && (bytes[i] != '\r')
+                                    && (bytes[i] != '\n')) i++;
+                            int wrapLen = i - wrapStart;
+                            if (i - start < sectionLen) {
+                                i++;
+                                if (bytes[i - 1] == '\r'
+                                    && i - start < sectionLen
+                                    && bytes[i] == '\n')
+                                        i++;
+                            }
 
                             nameBuf.write(bytes, wrapStart, wrapLen);
                         } else {
@@ -167,7 +190,7 @@
                         }
                     }
 
-                    entries.computeIfAbsent(new String(nameBuf.toByteArray(), UTF_8),
+                    entries.computeIfAbsent(nameBuf.toString(UTF_8),
                                             dummy -> new Entry())
                             .addSection(new Section(start, sectionLen,
                                     sectionLenWithBlank, rawBytes));
@@ -202,6 +225,26 @@
             return this;
         }
 
+        /**
+         * Check if the sections (particularly the last one of usually only one)
+         * are properly delimited with a trailing blank line so that another
+         * section can be correctly appended and return {@code true} or return
+         * {@code false} to indicate that reproduction is not advised and should
+         * be carried out with a clean "normalized" newly-written manifest.
+         *
+         * @see #reproduceRaw
+         */
+        public boolean isProperlyDelimited() {
+            return sections.stream().allMatch(
+                    Section::isProperlySectionDelimited);
+        }
+
+        public void reproduceRaw(OutputStream out) throws IOException {
+            for (Section sec : sections) {
+                out.write(sec.rawBytes, sec.offset, sec.lengthWithBlankLine);
+            }
+        }
+
         public byte[] digest(MessageDigest md)
         {
             md.reset();
@@ -242,6 +285,15 @@
             this.rawBytes = rawBytes;
         }
 
+        /**
+         * Returns {@code true} if the raw section is terminated with a blank
+         * line so that another section can possibly be appended resulting in a
+         * valid manifest and {@code false} otherwise.
+         */
+        private boolean isProperlySectionDelimited() {
+            return lengthWithBlankLine > length;
+        }
+
         private static void doOldStyle(MessageDigest md,
                                 byte[] bytes,
                                 int offset,
@@ -268,10 +320,33 @@
         }
     }
 
+    /**
+     * @see #MF_MAIN_ATTRS
+     */
+    public Entry getMainAttsEntry() {
+        return mainAttsEntry;
+    }
+
+    /**
+     * @see #MF_MAIN_ATTRS
+     */
+    public Entry getMainAttsEntry(boolean oldStyle) {
+        mainAttsEntry.oldStyle = oldStyle;
+        return mainAttsEntry;
+    }
+
+    public Entry get(String name) {
+        return entries.get(name);
+    }
+
     public Entry get(String name, boolean oldStyle) {
-        Entry e = entries.get(name);
-        if (e != null)
+        Entry e = get(name);
+        if (e == null && MF_MAIN_ATTRS.equals(name)) {
+            e = getMainAttsEntry();
+        }
+        if (e != null) {
             e.oldStyle = oldStyle;
+        }
         return e;
     }
 
--- a/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -72,8 +72,7 @@
     private ArrayList<CodeSigner[]> signerCache;
 
     private static final String ATTR_DIGEST =
-        ("-DIGEST-" + ManifestDigester.MF_MAIN_ATTRS).toUpperCase
-        (Locale.ENGLISH);
+        "-DIGEST-" + ManifestDigester.MF_MAIN_ATTRS.toUpperCase(Locale.ENGLISH);
 
     /** the PKCS7 block for this .DSA/.RSA/.EC file */
     private PKCS7 block;
@@ -537,8 +536,7 @@
 
                 MessageDigest digest = getDigest(algorithm);
                 if (digest != null) {
-                    ManifestDigester.Entry mde =
-                        md.get(ManifestDigester.MF_MAIN_ATTRS, false);
+                    ManifestDigester.Entry mde = md.getMainAttsEntry(false);
                     byte[] computedHash = mde.digest(digest);
                     byte[] expectedHash =
                         Base64.getMimeDecoder().decode((String)se.getValue());
--- a/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -671,26 +671,18 @@
             throw new AssertionError(asae);
         }
 
-        PrintStream ps = new PrintStream(os);
-        ZipOutputStream zos = new ZipOutputStream(ps);
+        ZipOutputStream zos = new ZipOutputStream(os);
 
         Manifest manifest = new Manifest();
-        Map<String, Attributes> mfEntries = manifest.getEntries();
-
-        // The Attributes of manifest before updating
-        Attributes oldAttr = null;
-
-        boolean mfModified = false;
-        boolean mfCreated = false;
         byte[] mfRawBytes = null;
 
         // Check if manifest exists
-        ZipEntry mfFile;
-        if ((mfFile = getManifestFile(zipFile)) != null) {
+        ZipEntry mfFile = getManifestFile(zipFile);
+        boolean mfCreated = mfFile == null;
+        if (!mfCreated) {
             // Manifest exists. Read its raw bytes.
             mfRawBytes = zipFile.getInputStream(mfFile).readAllBytes();
             manifest.read(new ByteArrayInputStream(mfRawBytes));
-            oldAttr = (Attributes) (manifest.getMainAttributes().clone());
         } else {
             // Create new manifest
             Attributes mattr = manifest.getMainAttributes();
@@ -701,7 +693,6 @@
             mattr.putValue("Created-By", jdkVersion + " (" + javaVendor
                     + ")");
             mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
-            mfCreated = true;
         }
 
         /*
@@ -728,8 +719,12 @@
                 // out first
                 mfFiles.addElement(ze);
 
-                if (SignatureFileVerifier.isBlockOrSF(
-                        ze.getName().toUpperCase(Locale.ENGLISH))) {
+                String zeNameUp = ze.getName().toUpperCase(Locale.ENGLISH);
+                if (SignatureFileVerifier.isBlockOrSF(zeNameUp)
+                    // no need to preserve binary manifest portions
+                    // if the only existing signature will be replaced
+                        && !zeNameUp.startsWith(SignatureFile
+                            .getBaseSignatureFilesName(signerName))) {
                     wasSigned = true;
                 }
 
@@ -742,55 +737,69 @@
             if (manifest.getAttributes(ze.getName()) != null) {
                 // jar entry is contained in manifest, check and
                 // possibly update its digest attributes
-                if (updateDigests(ze, zipFile, digests,
-                        manifest)) {
-                    mfModified = true;
-                }
+                updateDigests(ze, zipFile, digests, manifest);
             } else if (!ze.isDirectory()) {
                 // Add entry to manifest
                 Attributes attrs = getDigestAttributes(ze, zipFile, digests);
-                mfEntries.put(ze.getName(), attrs);
-                mfModified = true;
+                manifest.getEntries().put(ze.getName(), attrs);
             }
         }
 
-        // Recalculate the manifest raw bytes if necessary
-        if (mfModified) {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        /*
+         * Note:
+         *
+         * The Attributes object is based on HashMap and can handle
+         * continuation lines. Therefore, even if the contents are not changed
+         * (in a Map view), the bytes that it write() may be different from
+         * the original bytes that it read() from. Since the signature is
+         * based on raw bytes, we must retain the exact bytes.
+         */
+        boolean mfModified;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        if (mfCreated || !wasSigned) {
+            mfModified = true;
             manifest.write(baos);
-            if (wasSigned) {
-                byte[] newBytes = baos.toByteArray();
-                if (mfRawBytes != null
-                        && oldAttr.equals(manifest.getMainAttributes())) {
+            mfRawBytes = baos.toByteArray();
+        } else {
 
-                    /*
-                     * Note:
-                     *
-                     * The Attributes object is based on HashMap and can handle
-                     * continuation columns. Therefore, even if the contents are
-                     * not changed (in a Map view), the bytes that it write()
-                     * may be different from the original bytes that it read()
-                     * from. Since the signature on the main attributes is based
-                     * on raw bytes, we must retain the exact bytes.
-                     */
+            // the manifest before updating
+            Manifest oldManifest = new Manifest(
+                    new ByteArrayInputStream(mfRawBytes));
+            mfModified = !oldManifest.equals(manifest);
+            if (!mfModified) {
+                // leave whole manifest (mfRawBytes) unmodified
+            } else {
+                // reproduce the manifest raw bytes for unmodified sections
+                manifest.write(baos);
+                byte[] mfNewRawBytes = baos.toByteArray();
+                baos.reset();
 
-                    int newPos = findHeaderEnd(newBytes);
-                    int oldPos = findHeaderEnd(mfRawBytes);
+                ManifestDigester oldMd = new ManifestDigester(mfRawBytes);
+                ManifestDigester newMd = new ManifestDigester(mfNewRawBytes);
 
-                    if (newPos == oldPos) {
-                        System.arraycopy(mfRawBytes, 0, newBytes, 0, oldPos);
+                // main attributes
+                if (manifest.getMainAttributes().equals(
+                        oldManifest.getMainAttributes())
+                        && (manifest.getEntries().isEmpty() ||
+                            oldMd.getMainAttsEntry().isProperlyDelimited())) {
+                    oldMd.getMainAttsEntry().reproduceRaw(baos);
+                } else {
+                    newMd.getMainAttsEntry().reproduceRaw(baos);
+                }
+
+                // individual sections
+                for (Map.Entry<String,Attributes> entry :
+                        manifest.getEntries().entrySet()) {
+                    String sectionName = entry.getKey();
+                    Attributes entryAtts = entry.getValue();
+                    if (entryAtts.equals(oldManifest.getAttributes(sectionName))
+                            && oldMd.get(sectionName).isProperlyDelimited()) {
+                        oldMd.get(sectionName).reproduceRaw(baos);
                     } else {
-                        // cat oldHead newTail > newBytes
-                        byte[] lastBytes = new byte[oldPos +
-                                newBytes.length - newPos];
-                        System.arraycopy(mfRawBytes, 0, lastBytes, 0, oldPos);
-                        System.arraycopy(newBytes, newPos, lastBytes, oldPos,
-                                newBytes.length - newPos);
-                        newBytes = lastBytes;
+                        newMd.get(sectionName).reproduceRaw(baos);
                     }
                 }
-                mfRawBytes = newBytes;
-            } else {
+
                 mfRawBytes = baos.toByteArray();
             }
         }
@@ -801,13 +810,12 @@
             mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
         }
         if (handler != null) {
-            if (mfCreated) {
+            if (mfCreated || !mfModified) {
                 handler.accept("adding", mfFile.getName());
-            } else if (mfModified) {
+            } else {
                 handler.accept("updating", mfFile.getName());
             }
         }
-
         zos.putNextEntry(mfFile);
         zos.write(mfRawBytes);
 
@@ -826,9 +834,8 @@
         }
         signer.initSign(privateKey);
 
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        baos.reset();
         sf.write(baos);
-
         byte[] content = baos.toByteArray();
 
         signer.update(content);
@@ -889,6 +896,14 @@
             if (!ze.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME)
                     && !ze.getName().equalsIgnoreCase(sfFilename)
                     && !ze.getName().equalsIgnoreCase(bkFilename)) {
+                if (ze.getName().startsWith(SignatureFile
+                        .getBaseSignatureFilesName(signerName))
+                        && SignatureFileVerifier.isBlockOrSF(ze.getName())) {
+                    if (handler != null) {
+                        handler.accept("updating", ze.getName());
+                    }
+                    continue;
+                }
                 if (handler != null) {
                     if (manifest.getAttributes(ze.getName()) != null) {
                         handler.accept("signing", ze.getName());
@@ -942,11 +957,9 @@
         }
     }
 
-    private boolean updateDigests(ZipEntry ze, ZipFile zf,
+    private void updateDigests(ZipEntry ze, ZipFile zf,
                                   MessageDigest[] digests,
                                   Manifest mf) throws IOException {
-        boolean update = false;
-
         Attributes attrs = mf.getAttributes(ze.getName());
         String[] base64Digests = getDigests(ze, zf, digests);
 
@@ -976,19 +989,9 @@
 
             if (name == null) {
                 name = digests[i].getAlgorithm() + "-Digest";
-                attrs.putValue(name, base64Digests[i]);
-                update = true;
-            } else {
-                // compare digests, and replace the one in the manifest
-                // if they are different
-                String mfDigest = attrs.getValue(name);
-                if (!mfDigest.equalsIgnoreCase(base64Digests[i])) {
-                    attrs.putValue(name, base64Digests[i]);
-                    update = true;
-                }
             }
+            attrs.putValue(name, base64Digests[i]);
         }
-        return update;
     }
 
     private Attributes getDigestAttributes(
@@ -1051,30 +1054,6 @@
         return base64Digests;
     }
 
-    @SuppressWarnings("fallthrough")
-    private int findHeaderEnd(byte[] bs) {
-        // Initial state true to deal with empty header
-        boolean newline = true;     // just met a newline
-        int len = bs.length;
-        for (int i = 0; i < len; i++) {
-            switch (bs[i]) {
-                case '\r':
-                    if (i < len - 1 && bs[i + 1] == '\n') i++;
-                    // fallthrough
-                case '\n':
-                    if (newline) return i + 1;    //+1 to get length
-                    newline = true;
-                    break;
-                default:
-                    newline = false;
-            }
-        }
-        // If header end is not found, it means the MANIFEST.MF has only
-        // the main attributes section and it does not end with 2 newlines.
-        // Returns the whole length so that it can be completely replaced.
-        return len;
-    }
-
     /*
      * Try to load the specified signing mechanism.
      * The URL class loader is used.
@@ -1145,14 +1124,12 @@
             }
 
             // create digest of the manifest main attributes
-            ManifestDigester.Entry mde =
-                    md.get(ManifestDigester.MF_MAIN_ATTRS, false);
+            ManifestDigester.Entry mde = md.getMainAttsEntry(false);
             if (mde != null) {
-                for (MessageDigest digest: digests) {
-                    mattr.putValue(digest.getAlgorithm() +
-                                    "-Digest-" + ManifestDigester.MF_MAIN_ATTRS,
-                            Base64.getEncoder().encodeToString(
-                                    mde.digest(digest)));
+                for (MessageDigest digest : digests) {
+                    mattr.putValue(digest.getAlgorithm() + "-Digest-" +
+                            ManifestDigester.MF_MAIN_ATTRS,
+                            Base64.getEncoder().encodeToString(mde.digest(digest)));
                 }
             } else {
                 throw new IllegalStateException
@@ -1181,15 +1158,19 @@
             sf.write(out);
         }
 
+        private static String getBaseSignatureFilesName(String baseName) {
+            return "META-INF/" + baseName + ".";
+        }
+
         // get .SF file name
         public String getMetaName() {
-            return "META-INF/" + baseName + ".SF";
+            return getBaseSignatureFilesName(baseName) + "SF";
         }
 
         // get .DSA (or .DSA, .EC) file name
         public String getBlockName(PrivateKey privateKey) {
             String keyAlgorithm = privateKey.getAlgorithm();
-            return "META-INF/" + baseName + "." + keyAlgorithm;
+            return getBaseSignatureFilesName(baseName) + keyAlgorithm;
         }
 
         // Generates the PKCS#7 content of block file
--- a/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Thu Jul 18 08:53:06 2019 +0800
@@ -944,11 +944,10 @@
                     // Don't read from the newManifest InputStream, as we
                     // might need it below, and we can't re-read the same data
                     // twice.
-                    FileInputStream fis = new FileInputStream(mname);
-                    boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
-                    fis.close();
-                    if (ambiguous) {
-                        return false;
+                    try (FileInputStream fis = new FileInputStream(mname)) {
+                        if (isAmbiguousMainClass(new Manifest(fis))) {
+                            return false;
+                        }
                     }
                 }
                 // Update the manifest.
--- a/test/jdk/ProblemList.txt	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/ProblemList.txt	Thu Jul 18 08:53:06 2019 +0800
@@ -661,6 +661,7 @@
 sun/security/tools/keytool/KeyToolTest.java                     8224644 solaris-all
 sun/security/tools/keytool/WeakAlg.java                         8224644 solaris-all
 
+sun/security/tools/jarsigner/compatibility/SignTwice.java       8217375 windows-all
 sun/security/tools/jarsigner/warnings/BadKeyUsageTest.java      8026393 generic-all
 
 javax/net/ssl/ServerName/SSLEngineExplorerMatchedSNI.java       8212096 generic-all
--- a/test/jdk/sun/security/tools/jarsigner/DiffEnd.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/DiffEnd.java	Thu Jul 18 08:53:06 2019 +0800
@@ -23,10 +23,14 @@
 
 /*
  * @test
- * @bug 6948909
+ * @bug 6948909 8217375
  * @summary Jarsigner removes MANIFEST.MF info for badly packages jar's
  * @library /test/lib
  */
+/*
+ * See also InsufficientSectionDelimiter.java for similar tests including cases
+ * without or with different line breaks.
+ */
 
 import jdk.test.lib.Asserts;
 import jdk.test.lib.SecurityTools;
@@ -44,47 +48,47 @@
 public class DiffEnd {
 
     static void check() throws Exception {
-        SecurityTools.jarsigner("-keystore "
-                + Path.of(System.getProperty("test.src"), "JarSigning.keystore")
-                .toString()
-                + " -storepass bbbbbb -digestalg SHA1"
-                + " -signedjar diffend.new.jar diffend.jar c");
+        String ksArgs = "-keystore " + Path.of(System.getProperty("test.src"))
+                .resolve("JarSigning.keystore") + " -storepass bbbbbb";
 
-        try (JarFile jf = new JarFile("diffend.new.jar")) {
+        SecurityTools.jarsigner(ksArgs + " -digestalg SHA1 "
+                + "-signedjar diffend.signed.jar diffend.jar c")
+                .shouldHaveExitValue(0);
+        SecurityTools.jarsigner(" -verify " + ksArgs + " -verbose "
+                + "diffend.signed.jar c")
+                .stdoutShouldMatch("^smk .* 1$").shouldHaveExitValue(0);
+
+        try (JarFile jf = new JarFile("diffend.signed.jar")) {
             Asserts.assertTrue(jf.getManifest().getMainAttributes()
                     .containsKey(new Attributes.Name("Today")));
         }
     }
 
     public static void main(String[] args) throws Exception {
-
         // A MANIFEST.MF using \n as newlines and no double newlines at the end
-        byte[] manifest =
-                ("Manifest-Version: 1.0\n"
+        byte[] manifest = ("Manifest-Version: 1.0\n"
                         + "Created-By: 1.7.0-internal (Sun Microsystems Inc.)\n"
                         + "Today: Monday\n").getBytes(StandardCharsets.UTF_8);
 
+        // Without the fake .RSA file, to trigger the if (wasSigned) else block
+        try (FileOutputStream fos = new FileOutputStream("diffend.jar");
+             ZipOutputStream zos = new ZipOutputStream(fos)) {
+            zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
+            zos.write(manifest);
+            zos.putNextEntry(new ZipEntry("1"));
+            zos.write(new byte[10]);
+        }
+        check();
+
         // With the fake .RSA file, to trigger the if (wasSigned) block
         try (FileOutputStream fos = new FileOutputStream("diffend.jar");
              ZipOutputStream zos = new ZipOutputStream(fos)) {
-            zos.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
+            zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
             zos.write(manifest);
-            zos.putNextEntry(new ZipEntry("META-INF/x.RSA"));
+            zos.putNextEntry(new ZipEntry("META-INF/x.RSA")); // fake .RSA
             zos.putNextEntry(new ZipEntry("1"));
             zos.write(new byte[10]);
         }
-
-        check();
-
-        // Without the fake .RSA file, to trigger the else block
-        try (FileOutputStream fos = new FileOutputStream("diffend.jar");
-             ZipOutputStream zos = new ZipOutputStream(fos)) {
-            zos.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
-            zos.write(manifest);
-            zos.putNextEntry(new ZipEntry("1"));
-            zos.write(new byte[10]);
-        }
-
         check();
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/DigestDontIgnoreCase.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.JarEntry;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @run testng DigestDontIgnoreCase
+ * @summary Check that existing manifest digest entries are taken for valid
+ * only if they match the actual digest value also taking upper and lower
+ * case of the base64 encoded form of the digests into account.
+ */
+/*
+ * <pre>mfDigest.equalsIgnoreCase(base64Digests[i])</pre>
+ * previously in JarSigner.java on on line 985
+ * @see jdk.security.jarsigner.JarSigner#updateDigests
+ */
+public class DigestDontIgnoreCase {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    static final String DUMMY_FILE1 = "dummy1.txt";
+    static final byte[] DUMMY_CONTENTS1 = DUMMY_FILE1.getBytes(UTF_8);
+    static final String DUMMY_FILE2 = "dummy2.txt";
+    static final byte[] DUMMY_CONTENTS2 = DUMMY_FILE2.getBytes(UTF_8);
+
+    byte[] goodSignedManifest;
+
+    @BeforeClass
+    public void prepareCertificate() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=X").shouldHaveExitValue(0);
+    }
+
+    void prepareJarFile(String filename, Map<String, byte[]> contents)
+            throws IOException {
+        try (OutputStream out = Files.newOutputStream(Path.of(filename));
+                JarOutputStream jos = new JarOutputStream(out)) {
+            for (Map.Entry<String, byte[]> entry : contents.entrySet()) {
+                JarEntry je = new JarEntry(entry.getKey());
+                jos.putNextEntry(je);
+                jos.write(entry.getValue());
+                jos.closeEntry();
+            }
+        }
+    }
+
+    @BeforeClass(dependsOnMethods = "prepareCertificate")
+    public void prepareGoodSignedManifest() throws Exception {
+        String filename = "prepare.jar";
+        prepareJarFile(filename, Map.of(DUMMY_FILE1, DUMMY_CONTENTS1));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + filename + " a")
+                .shouldHaveExitValue(0);
+        goodSignedManifest = Utils.readJarManifestBytes(filename);
+        Utils.echoManifest(goodSignedManifest,
+                "reference manifest with one file signed");
+    }
+
+    void testWithManifest(String filename, byte[] manifestBytes)
+            throws Exception {
+        Utils.echoManifest(manifestBytes,
+                "going to test " + filename + " with manifest");
+        prepareJarFile(filename, Map.of(
+                JarFile.MANIFEST_NAME, manifestBytes,
+                DUMMY_FILE1, DUMMY_CONTENTS1, // with digest already in manifest
+                DUMMY_FILE2, DUMMY_CONTENTS2)); // causes manifest update
+        Utils.echoManifest(Utils.readJarManifestBytes(filename),
+                filename + " created with manifest");
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + filename + " a")
+                .shouldHaveExitValue(0);
+        Utils.echoManifest(Utils.readJarManifestBytes(filename),
+                filename + " signed resulting in manifest");
+        SecurityTools.jarsigner("-verify -strict -keystore " +
+                KEYSTORE_FILENAME + " -storepass changeit -debug -verbose " +
+                filename + " a").shouldHaveExitValue(0);
+    }
+
+    @Test
+    public void verifyDigestGoodCase() throws Exception {
+        testWithManifest("good.jar", goodSignedManifest);
+    }
+
+    @Test
+    public void testDigestHeaderNameCase() throws Exception {
+        byte[] mfBadHeader = new String(goodSignedManifest, UTF_8).
+                replace("SHA-256-Digest", "sha-256-dIGEST").getBytes(UTF_8);
+        testWithManifest("switch-header-name-case.jar", mfBadHeader);
+    }
+
+    @Test
+    public void testDigestWrongCase() throws Exception {
+        byte[] mfBadDigest = switchCase(goodSignedManifest, "Digest");
+        testWithManifest("switch-digest-case.jar", mfBadDigest);
+    }
+
+    byte[] switchCase(byte[] manifest, String attrName) {
+        byte[] wrongCase = Arrays.copyOf(manifest, manifest.length);
+        byte[] name = (attrName + ":").getBytes(UTF_8);
+        int matched = 0; // number of bytes before position i matching attrName
+        for (int i = 0; i < wrongCase.length; i++) {
+            if (wrongCase[i] == '\r' &&
+                    (i == wrongCase.length - 1 || wrongCase[i + 1] == '\n')) {
+                continue;
+            } else if ((wrongCase[i] == '\r' || wrongCase[i] == '\n')
+                    && (i == wrongCase.length - 1 || wrongCase[i + 1] != ' ')) {
+                matched = 0;
+            } else if (matched == name.length) {
+                wrongCase[i] = switchCase(wrongCase[i]);
+            } else if (name[matched] == wrongCase[i]) {
+                matched++;
+            } else {
+                matched = 0;
+            }
+        }
+        return wrongCase;
+    }
+
+    byte switchCase(byte c) {
+        if (c >= 'A' && c <= 'Z') {
+            return (byte) ('a' + (c - 'A'));
+        } else if (c >= 'a' && c <= 'z') {
+            return (byte) ('A' + (c - 'a'));
+        } else {
+            return c;
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/EmptyIndividualSectionName.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
+import java.nio.file.Path;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
+
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @modules java.base/java.util.jar:+open
+ * @run testng/othervm EmptyIndividualSectionName
+ * @summary Check that an individual section with an empty name is digested
+ * and signed.
+ * <p>
+ * See also
+ * jdk/test/jdk/sun/security/util/ManifestDigester/FindSections.java
+ * for much more detailed api level tests
+ */
+public class EmptyIndividualSectionName {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    @BeforeClass
+    public void prepareCertificate() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg EC -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit "
+                + "-alias a -dname CN=X").shouldHaveExitValue(0);
+    }
+
+    /**
+     * Adds an additional section with name {@code sectionName} to the manifest
+     * of a JAR before signing it with {@code signOpts}.
+     * @return signature file {@code META-INF/A.SF} for further assertions
+     */
+    Manifest test(String sectionName, String signOpts) throws Exception {
+        Manifest mf = new Manifest();
+        mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+        mf.getEntries().put(sectionName, new Attributes());
+        String jarFilename = "test" + sectionName +
+                (signOpts != null ? signOpts : "") + ".jar";
+        JarUtils.createJarFile(Path.of(jarFilename), mf, Path.of("."));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " +
+                (signOpts != null ? signOpts + " " : "") + jarFilename + " a")
+                .shouldHaveExitValue(0);
+        SecurityTools.jarsigner("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilename + " a")
+                .shouldHaveExitValue(0);
+
+        byte[] mfBytes = Utils.readJarManifestBytes(jarFilename);
+        Utils.echoManifest(mfBytes, "manifest");
+        mf = new Manifest(new ByteArrayInputStream(mfBytes));
+        assertNotNull(mf.getAttributes(sectionName));
+        byte[] sfBytes = Utils.readJarEntryBytes(jarFilename, "META-INF/A.SF");
+        Utils.echoManifest(sfBytes, "signature file META-INF/A.SF");
+        return new Manifest(new ByteArrayInputStream(sfBytes));
+    }
+
+    /**
+     * Verifies that it makes a difference if the name is empty or not
+     * by running the same test as {@link #testNameEmpty} with only a different
+     * section name.
+     */
+    @Test
+    public void testNameNotEmpty() throws Exception {
+        String sectionName = "X";
+        assertNotNull(test(sectionName, null).getAttributes(sectionName));
+    }
+
+    /**
+     * Verifies that individual sections are digested and signed also if the
+     * name of such a section is empty.
+     * An empty name of an individual section cannot be tested by adding a file
+     * with an empty name to a JAR because such a file name is invalid and
+     * cannot be used to add a file because it cannot be created or added to
+     * the JAR file in the first place. However, an individual section with an
+     * empty name can be added to the manifest.
+     * Expected is a corresponding digest in the signature file which was not
+     * present or produced before resolution of bug 8217375.
+     */
+    @Test
+    public void testNameEmpty() throws Exception {
+        String sectionName = "";
+        assertNotNull(test(sectionName, null).getAttributes(sectionName));
+    }
+
+    /**
+     * Similar to {@link #testNameEmpty} but tries to show a real difference
+     * rather than just some internals in a {@code .SF} file, but TODO
+     */
+    @Test(enabled = false, description = "TODO")
+    public void testNameEmptyTrusted() throws Exception {
+        String sectionName = "";
+        test(sectionName, "-sectionsonly");
+        String jarFilename = "test" + sectionName + "-sectionsonly.jar";
+        try (JarFile jar = new JarFile(jarFilename, true)) {
+            Manifest m = jar.getManifest();
+            Method getTrustedAttributes = m.getClass()
+                    .getDeclaredMethod("getTrustedAttributes", String.class);
+            getTrustedAttributes.setAccessible(true);
+            assertThrows(SecurityException.class, () ->
+                    getTrustedAttributes.invoke(m, sectionName));
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/EmptyJar.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.nio.file.Path;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes.Name;
+
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @library /test/lib /lib/testlibrary
+ * @run testng EmptyJar
+ * @summary Checks that signing an empty jar file does not result in an NPE or
+ * other error condition.
+ */
+public class EmptyJar {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    @BeforeClass
+    public void prepareKeyStore() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg EC -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=A").shouldHaveExitValue(0);
+    }
+
+    @Test
+    public void test() throws Exception {
+        String jarFilename = "test.jar";
+        JarUtils.createJarFile(Path.of(jarFilename), (Manifest) null,
+                Path.of("."));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + jarFilename + " a")
+                .shouldHaveExitValue(0);
+
+        // verify that jarsigner has added a default manifest
+        byte[] mfBytes = Utils.readJarManifestBytes(jarFilename);
+        Utils.echoManifest(mfBytes, "manifest");
+        assertTrue(new String(mfBytes, UTF_8).startsWith(
+                Name.MANIFEST_VERSION + ": 1.0\r\nCreated-By: "));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/FindHeaderEndVsManifestDigesterFindFirstSection.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import sun.security.util.ManifestDigester;
+
+import org.testng.annotations.Test;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Factory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @run testng FindHeaderEndVsManifestDigesterFindFirstSection
+ * @summary Checks that {@link JarSigner#findHeaderEnd} (moved to now
+ * {@link #findHeaderEnd} in this test) can be replaced with
+ * {@link ManifestDigester#findSection}
+ * (first invocation will identify main attributes)
+ * without making a difference.
+ */
+/*
+ * Note to future maintainer:
+ * While it might look at first glance like this test ensures backwards-
+ * compatibility between JarSigner.findHeaderEnd and
+ * ManifestDigester.findSection's first invocation that find the main
+ * attributes section, at the time of that change, this test continues to
+ * verify main attributes digestion now with ManifestDigester.findSection as
+ * opposed to previous implementation in JarSigner.findHeaderEnd.
+ * Before completely removing this test, make sure that main attributes
+ * digestion is covered appropriately with tests. After JarSigner.findHeaderEnd
+ * has been removed digests should still continue to match.
+ *
+ * See also
+ * - jdk/test/jdk/sun/security/tools/jarsigner/PreserveRawManifestEntryAndDigest.java
+ * for some end-to-end tests utilizing the jarsigner tool,
+ * - jdk/test/jdk/sun/security/util/ManifestDigester/FindSection.java and
+ * - jdk/test/jdk/sun/security/util/ManifestDigester/DigestInput.java
+ * for much more detailed tests at api level
+ *
+ * Both test mentioned above, however, originally were created when removing
+ * confusion of "Manifest-Main-Attributes" individual section with actual main
+ * attributes whereas the test here is about changes related to raw manifest
+ * reproduction and in the end test pretty much the same behavior.
+ */
+public class FindHeaderEndVsManifestDigesterFindFirstSection {
+
+    static final boolean FIXED_8217375 = true; // FIXME
+
+    /**
+     * from former {@link JarSigner#findHeaderEnd}, subject to verification if
+     * it can be replaced with {@link ManifestDigester#findSection}
+     */
+    @SuppressWarnings("fallthrough")
+    private int findHeaderEnd(byte[] bs) {
+        // Initial state true to deal with empty header
+        boolean newline = true;     // just met a newline
+        int len = bs.length;
+        for (int i = 0; i < len; i++) {
+            switch (bs[i]) {
+                case '\r':
+                    if (i < len - 1 && bs[i + 1] == '\n') i++;
+                    // fallthrough
+                case '\n':
+                    if (newline) return i + 1;    //+1 to get length
+                    newline = true;
+                    break;
+                default:
+                    newline = false;
+            }
+        }
+        // If header end is not found, it means the MANIFEST.MF has only
+        // the main attributes section and it does not end with 2 newlines.
+        // Returns the whole length so that it can be completely replaced.
+        return len;
+    }
+
+    @DataProvider(name = "parameters")
+    public static Object[][] parameters() {
+        List<Object[]> tests = new ArrayList<>();
+        for (String lineBreak : new String[] { "\n", "\r", "\r\n" }) {
+            if ("\r".equals(lineBreak) && !FIXED_8217375) continue;
+            for (int numLBs = 0; numLBs <= 3; numLBs++) {
+                for (String addSection : new String[] { null, "Ignore" }) {
+                    tests.add(new Object[] { lineBreak, numLBs, addSection });
+                }
+            }
+        }
+        return tests.toArray(new Object[tests.size()][]);
+    }
+
+    @Factory(dataProvider = "parameters")
+    public static Object[] createTests(String lineBreak, int numLineBreaks,
+            String individualSectionName) {
+        return new Object[]{new FindHeaderEndVsManifestDigesterFindFirstSection(
+                lineBreak, numLineBreaks, individualSectionName
+        )};
+    }
+
+    final String lineBreak;
+    final int numLineBreaks; // number of line breaks after main attributes
+    final String individualSectionName; // null means only main attributes
+    final byte[] rawBytes;
+
+    FindHeaderEndVsManifestDigesterFindFirstSection(String lineBreak,
+            int numLineBreaks, String individualSectionName) {
+        this.lineBreak = lineBreak;
+        this.numLineBreaks = numLineBreaks;
+        this.individualSectionName = individualSectionName;
+
+        rawBytes = (
+            "oldStyle: trailing space " + lineBreak +
+            "newStyle: no trailing space" + lineBreak.repeat(numLineBreaks) +
+            // numLineBreaks < 2 will not properly delimit individual section
+            // but it does not hurt to test that anyway
+            (individualSectionName != null ?
+                    "Name: " + individualSectionName + lineBreak +
+                    "Ignore: nothing here" + lineBreak +
+                    lineBreak
+                : "")
+        ).getBytes(UTF_8);
+    }
+
+    @BeforeMethod
+    public void verbose() {
+        System.out.println("lineBreak = " + stringToIntList(lineBreak));
+        System.out.println("numLineBreaks = " + numLineBreaks);
+        System.out.println("individualSectionName = " + individualSectionName);
+    }
+
+    @FunctionalInterface
+    interface Callable {
+        void call() throws Exception;
+    }
+
+    void catchNoLineBreakAfterMainAttributes(Callable test) throws Exception {
+        // manifests cannot be parsed and digested if the main attributes do
+        // not end in a blank line (double line break) or one line break
+        // immediately before eof.
+        boolean failureExpected = numLineBreaks == 0
+                && individualSectionName == null;
+        try {
+            test.call();
+            if (failureExpected) fail("expected an exception");
+        } catch (NullPointerException | IllegalStateException e) {
+            if (!failureExpected) fail("unexpected " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Checks that the beginning of the manifest until position<ol>
+     * <li>{@code Jarsigner.findHeaderEnd} in the previous version
+     * and</li>
+     * <li>{@code ManifestDigester.getMainAttsEntry().sections[0].
+     * lengthWithBlankLine} in the new version</li>
+     * </ol>produce the same offset (TODO: or the same error).
+     * The beginning of the manifest until that offset (range
+     * <pre>0 .. (offset - 1)</pre>) will be reproduced if the manifest has
+     * not changed.
+     * <p>
+     * Getting {@code startOfNext} of {@link ManifestDigester#findSection}'s
+     * first invokation returned {@link ManifestDigester.Position} which
+     * identifies the end offset of the main attributes is difficulted by
+     * {@link ManifestDigester#findSection} being private and therefore not
+     * directly accessible.
+     */
+    @Test
+    public void startOfNextLengthWithBlankLine() throws Exception {
+        catchNoLineBreakAfterMainAttributes(() ->
+            assertEquals(lengthWithBlankLine(), findHeaderEnd(rawBytes))
+        );
+    }
+
+    /**
+     * Due to its private visibility,
+     * {@link ManifestDigester.Section#lengthWithBlankLine} is not directly
+     * accessible. However, calling {@link ManifestDigester.Entry#digest}
+     * reveals {@code lengthWithBlankLine} as third parameter in
+     * <pre>md.update(sec.rawBytes, sec.offset, sec.lengthWithBlankLine);</pre>
+     * on line ManifestDigester.java:212.
+     * <p>
+     * This value is the same as {@code startOfNext} of
+     * {@link ManifestDigester#findSection}'s first invocation returned
+     * {@link ManifestDigester.Position} identifying the end offset of the
+     * main attributes because<ol>
+     * <li>the end offset of the main attributes is assigned to
+     * {@code startOfNext} in
+     * <pre>pos.startOfNext = i+1;</pre> in ManifestDigester.java:98</li>
+     * <li>which is then passed on as the third parameter to the constructor
+     * of a new {@link ManifestDigester.Section#Section} by
+     * <pre>new Section(0, pos.endOfSection + 1, pos.startOfNext, rawBytes)));</pre>
+     * in in ManifestDigester.java:128</li>
+     * <li>where it is assigned to
+     * {@link ManifestDigester.Section#lengthWithBlankLine} by
+     * <pre>this.lengthWithBlankLine = lengthWithBlankLine;</pre>
+     * in ManifestDigester.java:241</li>
+     * <li>from where it is picked up by {@link ManifestDigester.Entry#digest}
+     * in
+     * <pre>md.update(sec.rawBytes, sec.offset, sec.lengthWithBlankLine);</pre>
+     * in ManifestDigester.java:212</li>
+     * </ol>
+     * all of which without any modification.
+     */
+    int lengthWithBlankLine() {
+        int[] lengthWithBlankLine = new int[] { 0 };
+        new ManifestDigester(rawBytes).get(ManifestDigester.MF_MAIN_ATTRS,
+                false).digest(new MessageDigest("lengthWithBlankLine") {
+            @Override protected void engineReset() {
+                lengthWithBlankLine[0] = 0;
+            }
+            @Override protected void engineUpdate(byte b) {
+                lengthWithBlankLine[0]++;
+            }
+            @Override protected void engineUpdate(byte[] b, int o, int l) {
+                lengthWithBlankLine[0] += l;
+            }
+            @Override protected byte[] engineDigest() {
+                return null;
+            }
+        });
+        return lengthWithBlankLine[0];
+    }
+
+    /**
+     * Checks that the replacement of {@link JarSigner#findHeaderEnd} is
+     * actually used to reproduce manifest main attributes.
+     * <p>
+     * {@link #startOfNextLengthWithBlankLine} demonstrates that
+     * {@link JarSigner#findHeaderEnd} has been replaced successfully with
+     * {@link ManifestDigester#findSection} but does not also show that the
+     * main attributes are reproduced with the same offset as before.
+     * {@link #startOfNextLengthWithBlankLine} uses
+     * {@link ManifestDigester.Entry#digest} to demonstrate an equal offset
+     * calculated but {@link ManifestDigester.Entry#digest} is not necessarily
+     * the same as reproducing, especially when considering
+     * {@link ManifestDigester.Entry#oldStyle}.
+     */
+    @Test(enabled = FIXED_8217375)
+    public void reproduceMainAttributes() throws Exception {
+        catchNoLineBreakAfterMainAttributes(() -> {
+            ByteArrayOutputStream buf = new ByteArrayOutputStream();
+            ManifestDigester md = new ManifestDigester(rawBytes);
+            // without 8217375 fixed the following line will not even compile
+            // so just remove it and skip the test for regression
+            md.getMainAttsEntry().reproduceRaw(buf); // FIXME
+
+            assertEquals(buf.size(), findHeaderEnd(rawBytes));
+        });
+    }
+
+    static List<Integer> stringToIntList(String string) {
+        byte[] bytes = string.getBytes(UTF_8);
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < bytes.length; i++) {
+            list.add((int) bytes[i]);
+        }
+        return list;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/InsufficientSectionDelimiter.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.stream.Stream;
+import java.util.jar.Attributes.Name;
+import java.util.jar.Manifest;
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Factory;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @run testng InsufficientSectionDelimiter
+ * @summary Checks some cases signing a jar the manifest of which has no or
+ * only one line break at the end and no proper delimiting blank line does not
+ * result in an invalid signed jar without jarsigner noticing and failing.
+ *
+ * <p>See also<ul>
+ * <li>{@link PreserveRawManifestEntryAndDigest} with an update of a signed
+ * jar with a different signer whereas this test just signs with one signer
+ * </li>
+ * <li>{@link WasSignedByOtherSigner} for a test that detects if
+ * {@code wasSigned} in {@link jdk.security.jarsigner.JarSigner#sign0} was set
+ * correctly determining whether or not to re-write the manifest, and</li>
+ * <li>{@code diffend.sh} for another similar test</li></ul>
+ */
+public class InsufficientSectionDelimiter {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    @BeforeTest
+    public void prepareCertificate() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg EC -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=A").shouldHaveExitValue(0);
+    }
+
+    @BeforeTest
+    public void prepareFakeSfFile() throws IOException {
+        new File("META-INF").mkdir();
+        Files.write(Path.of("META-INF/.SF"), (
+                Name.SIGNATURE_VERSION + ": 1.0\r\n" +
+                "-Digest-Manifest: \r\n\r\n").getBytes(UTF_8));
+    }
+
+    @DataProvider(name = "parameters")
+    public static Object[][] parameters() {
+        return new String[][] { { "" }, { "\n" }, { "\r" }, { "\r\n" } };
+    }
+
+    @Factory(dataProvider = "parameters")
+    public static Object[] createTests(String lineBreak) {
+        return new Object[] { new InsufficientSectionDelimiter(lineBreak) };
+    }
+
+    final String lineBreak;
+    final String jarFilenameSuffix;
+
+    InsufficientSectionDelimiter(String lineBreak) {
+        this.lineBreak = lineBreak;
+        jarFilenameSuffix = Utils.escapeStringWithNumbers(lineBreak);
+    }
+
+    @BeforeMethod
+    public void verbose() {
+        System.out.println("lineBreak = "
+                + Utils.escapeStringWithNumbers(lineBreak));
+    }
+
+    void test(String jarFilenamePrefix, String... files) throws Exception {
+        String jarFilename = jarFilenamePrefix + jarFilenameSuffix + ".jar";
+        JarUtils.createJarFile(Path.of(jarFilename), new Manifest() {
+            @Override public void write(OutputStream out) throws IOException {
+                out.write((Name.MANIFEST_VERSION + ": 1.0" +
+                        lineBreak).getBytes(UTF_8));
+            }
+        }, Path.of("."), Stream.of(files).map(Path::of).toArray(Path[]::new)
+        );
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename), "unsigned jar");
+        try {
+            SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                    " -storepass changeit -verbose -debug " + jarFilename +
+                    " a").shouldHaveExitValue(0);
+            Utils.echoManifest(Utils.readJarManifestBytes(
+                    jarFilename), "signed jar");
+        } catch (Exception e) {
+            if (lineBreak.isEmpty()) {
+                return; // invalid manifest without trailing line break
+            }
+            throw e;
+        }
+
+        // remove META-INF/.SF from signed jar which would not validate
+        // (not added in all the test cases)
+        JarUtils.updateJar(jarFilename, "verify-" + jarFilename,
+                Map.of("META-INF/.SF", false));
+        SecurityTools.jarsigner("-verify -strict -keystore " +
+                KEYSTORE_FILENAME + " -storepass changeit -debug -verbose " +
+                "verify-" + jarFilename + " a").shouldHaveExitValue(0);
+    }
+
+    /**
+     * Test that signing a jar which has never been signed yet and contains
+     * no signature related files with a manifest that ends immediately after
+     * the last main attributes value byte or only one line break and no blank
+     * line produces a valid signed jar or an error if the manifest ends
+     * without line break.
+     */
+    @Test
+    public void testOnlyMainAttrs() throws Exception {
+        test("testOnlyMainAttrs");
+    }
+
+    /**
+     * Test that signing a jar with a manifest that ends immediately after
+     * the last main attributes value byte or with too few line break
+     * characters to properly delimit an individual section and has a fake
+     * signing related file to trigger a signature update or more specifically
+     * wasSigned in JarSigner.sign0 to become true produces a valid signed jar
+     * or an error if the manifest ends without line break.
+     * <p>
+     * Only one line break and hence no blank line ('\r', '\n', or '\r\n')
+     * after last main attributes value byte is too little to delimit an
+     * individual section to hold a file's digest but acceptable if no
+     * individual section has to be added because no contained file has to be
+     * signed as is the case in this test.
+     *
+     * @see #testMainAttrsWasSignedAddFile
+     */
+    @Test
+    public void testMainAttrsWasSigned() throws Exception {
+        test("testMainAttrsWasSigned", "META-INF/.SF");
+    }
+
+    /**
+     * Test that signing a jar with a manifest that ends immediately after
+     * the last main attributes value byte or with too few line break
+     * characters to properly delimit an individual section and has a fake
+     * signing related file to trigger a signature update or more specifically
+     * wasSigned in JarSigner.sign0 to become true produces no invalid signed
+     * jar or an error if the manifest ends without line break.
+     * <p>
+     * Only one line break and hence no blank line ('\r', '\n', or '\r\n')
+     * after the last main attributes value byte is too little to delimit an
+     * individual section which would be required here to save the digest of a
+     * contained file to be signed.
+     * <p>
+     * Changing the delimiters after the main attributes changes the main
+     * attributes digest but
+     * {@link SignatureFileVerifier#verifyManifestMainAttrs} and
+     * {@link ManifestDigester#digestWorkaround} work around it.
+     */
+    @Test
+    public void testMainAttrsWasSignedAddFile() throws Exception {
+        Files.write(Path.of("test.txt"), "test.txt".getBytes(UTF_8));
+        test("testMainAttrsWasSignedAddFile", "META-INF/.SF", "test.txt");
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/MainAttributesConfused.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
+import sun.security.util.ManifestDigester;
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @library /test/lib /lib/testlibrary
+ * @run testng MainAttributesConfused
+ * @summary Check that manifest individual section "Manifest-Main-Attributes"
+ * does not interfere and is not confused with ManifestDigester internals.
+ *
+ * See also
+ * jdk/test/jdk/sun/security/util/ManifestDigester/ManifestMainAttributes.java
+ * for much more detailed api level tests
+ */
+public class MainAttributesConfused {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+    static final String MAIN_ATTRIBUTES_MARKER = null;
+
+    @BeforeClass
+    void prepareKeyStore() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg EC -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=X").shouldHaveExitValue(0);
+    }
+
+    void testAddManifestSection(String sectionName) throws Exception {
+        // create a signed jar
+        Manifest manifest = new Manifest();
+        manifest.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+        String testFile = "test-" + sectionName;
+        Files.write(Path.of(testFile), testFile.getBytes(UTF_8));
+        String jarFilename = sectionName + ".jar";
+        JarUtils.createJarFile(Path.of(jarFilename), manifest,
+                Path.of("."), Path.of(testFile));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + jarFilename + " a")
+                .shouldHaveExitValue(0);
+
+        // get the manifest of the signed jar with the signature digests, add
+        // a new individual section, and write it back
+        try (JarFile jar = new JarFile(jarFilename)) {
+            manifest = jar.getManifest();
+        }
+        Attributes attrs = sectionName == MAIN_ATTRIBUTES_MARKER
+                ? manifest.getMainAttributes()
+                : manifest.getEntries().computeIfAbsent(sectionName,
+                        n -> new Attributes());
+        attrs.put(new Name("Some-Key"), "Some-Value");
+        String jarFilenameAttrs = sectionName + "-attrs.jar";
+        JarUtils.updateManifest(jarFilename, jarFilenameAttrs, manifest);
+
+        // having just added another manifest entry (individual section) not
+        // modifying existing digests or main attributes should not invalidate
+        // the existing signature.
+        SecurityTools.jarsigner("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilenameAttrs +
+                " a").shouldHaveExitValue(0);
+    }
+
+    @Test
+    public void testAddOtherThanManifestMainAttributes() throws Exception {
+        // any value but "Manifest-Main-Attributes", even lower case works
+        testAddManifestSection("manifest-main-attributes");
+    }
+
+    @Test
+    public void testAddMainAttributesHeader() throws Exception {
+        // adding or changing existing attributes of the main section, however,
+        // will invalidate the signature
+        assertThrows(() -> testAddManifestSection(MAIN_ATTRIBUTES_MARKER));
+    }
+
+    @Test
+    public void testAddManifestMainAttributesSection() throws Exception {
+        testAddManifestSection(ManifestDigester.MF_MAIN_ATTRS);
+    }
+
+}
--- a/test/jdk/sun/security/tools/jarsigner/OldSig.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/OldSig.java	Thu Jul 18 08:53:06 2019 +0800
@@ -23,10 +23,15 @@
 
 /*
  * @test
- * @bug 6543940 6868865
+ * @bug 6543940 6868865 8217375
  * @summary Exception thrown when signing a jarfile in java 1.5
  * @library /test/lib
  */
+/*
+ * See also PreserveRawManifestEntryAndDigest.java for tests with arbitrarily
+ * formatted individual sections in addition the the main attributes tested
+ * here.
+ */
 
 import jdk.test.lib.SecurityTools;
 import jdk.test.lib.util.JarUtils;
@@ -44,8 +49,11 @@
         JarUtils.updateJarFile(Path.of("B.jar"), Path.of("."),
                 Path.of("B.class"));
 
-        SecurityTools.jarsigner("-keystore " + src.resolve("JarSigning.keystore")
-                + " -storepass bbbbbb -digestalg SHA1 B.jar c");
-        SecurityTools.jarsigner("-verify B.jar");
+        String ksArgs = "-keystore " + src.resolve("JarSigning.keystore")
+                + " -storepass bbbbbb";
+        SecurityTools.jarsigner(ksArgs + " -digestalg SHA1 B.jar c");
+        SecurityTools.jarsigner("-verify B.jar").shouldHaveExitValue(0);
+        SecurityTools.jarsigner("-verify " + ksArgs + " -verbose B.jar c")
+                .stdoutShouldMatch("^smk .* B[.]class$").shouldHaveExitValue(0);
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/PreserveRawManifestEntryAndDigest.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,1016 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.function.Function;
+import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
+import java.util.jar.Manifest;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipEntry;
+import jdk.test.lib.process.OutputAnalyzer;
+import jdk.test.lib.Platform;
+import jdk.test.lib.SecurityTools;
+import jdk.test.lib.util.JarUtils;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @modules jdk.jartool/sun.security.tools.jarsigner
+ * @run testng/timeout=1200 PreserveRawManifestEntryAndDigest
+ * @summary Verifies that JarSigner does not change manifest file entries
+ * in a binary view if its decoded map view does not change so that an
+ * unchanged (individual section) entry continues to produce the same digest.
+ * The same manifest (in terms of {@link Manifest#equals}) could be encoded
+ * with different line breaks ("{@code \r}", "{@code \n}", or "{@code \r\n}")
+ * or with arbitrary line break positions (as is also the case with the change
+ * of the default line width in JDK 11, bug 6372077) resulting in a different
+ * digest for manifest entries with identical values.
+ *
+ * <p>See also:<ul>
+ * <li>{@code oldsig.sh} and {@code diffend.sh} in
+ * {@code /test/jdk/sun/security/tools/jarsigner/}</li>
+ * <li>{@code Compatibility.java} in
+ * {@code /test/jdk/sun/security/tools/jarsigner/compatibility}</li>
+ * <li>{@link ReproduceRaw} testing relevant
+ * {@sun.security.util.ManifestDigester} api in much more detail</li>
+ * </ul>
+ */
+/*
+ * debug with "run testng" += "/othervm -Djava.security.debug=jar"
+ */
+public class PreserveRawManifestEntryAndDigest {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+    static final String FILENAME_INITIAL_CONTENTS = "initial-contents";
+    static final String FILENAME_UPDATED_CONTENTS = "updated-contents";
+
+    /**
+     * @see sun.security.tools.jarsigner.Main#run
+     */
+    static final int NOTSIGNEDBYALIASORALIASNOTINSTORE = 32;
+
+    @BeforeTest
+    public void prepareContentFiles() throws IOException {
+        Files.write(Path.of(FILENAME_INITIAL_CONTENTS),
+                FILENAME_INITIAL_CONTENTS.getBytes(UTF_8));
+        Files.write(Path.of(FILENAME_UPDATED_CONTENTS),
+                FILENAME_UPDATED_CONTENTS.getBytes(UTF_8));
+    }
+
+    @BeforeTest
+    public void prepareCertificates() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=A").shouldHaveExitValue(0);
+        SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias b -dname CN=B").shouldHaveExitValue(0);
+    }
+
+    static class TeeOutputStream extends FilterOutputStream {
+        final OutputStream tee; // don't flush or close
+
+        public TeeOutputStream(OutputStream out, OutputStream tee) {
+            super(out);
+            this.tee = tee;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            super.write(b);
+            tee.write(b);
+        }
+    }
+
+    /**
+     * runs jarsigner in its own child process and captures exit code and the
+     * output of stdout and stderr, as opposed to {@link #karsignerMain}
+     */
+    OutputAnalyzer jarsignerProc(String args) throws Exception {
+        long start = System.currentTimeMillis();
+        try {
+            return SecurityTools.jarsigner(args);
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("jarsignerProc duration [ms]: " + (end - start));
+        }
+    }
+
+    /**
+     * assume non-zero exit code would call System.exit but is faster than
+     * {@link #jarsignerProc}
+     */
+    void jarsignerMain(String args) throws Exception {
+        long start = System.currentTimeMillis();
+        try {
+            new sun.security.tools.jarsigner.Main().run(args.split("\\s+"));
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("jarsignerMain duration [ms]: " + (end - start));
+        }
+    }
+
+    void createSignedJarA(String jarFilename, Manifest manifest,
+            String additionalJarsignerOptions, String dummyContentsFilename)
+                    throws Exception {
+        JarUtils.createJarFile(Path.of(jarFilename), manifest, Path.of("."),
+                dummyContentsFilename == null ? new Path[]{} :
+                    new Path[] { Path.of(dummyContentsFilename) });
+        jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
+                + (additionalJarsignerOptions == null ? "" :
+                    " " + additionalJarsignerOptions) +
+                " -verbose -debug " + jarFilename + " a");
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename), "original signed jar by signer a");
+        // check assumption that jar is valid at this point
+        jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + jarFilename + " a");
+    }
+
+    void manipulateManifestSignAgainA(String srcJarFilename, String tmpFilename,
+            String dstJarFilename, String additionalJarsignerOptions,
+            Function<Manifest, byte[]> manifestManipulation) throws Exception {
+        Manifest mf;
+        try (JarFile jar = new JarFile(srcJarFilename)) {
+            mf = jar.getManifest();
+        }
+        byte[] manipulatedManifest = manifestManipulation.apply(mf);
+        Utils.echoManifest(manipulatedManifest, "manipulated manifest");
+        JarUtils.updateJar(srcJarFilename, tmpFilename, Map.of(
+                JarFile.MANIFEST_NAME, manipulatedManifest,
+                // add a fake sig-related file to trigger wasSigned in JarSigner
+                "META-INF/.SF", Name.SIGNATURE_VERSION + ": 1.0\r\n"));
+        jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
+                + (additionalJarsignerOptions == null ? "" :
+                    " " + additionalJarsignerOptions) +
+                " -verbose -debug " + tmpFilename + " a");
+        // remove META-INF/.SF from signed jar again which would not validate
+        JarUtils.updateJar(tmpFilename, dstJarFilename,
+                Map.of("META-INF/.SF", false));
+
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                dstJarFilename), "manipulated jar signed again with a");
+        // check assumption that jar is valid at this point
+        jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME + " " +
+                "-storepass changeit -verbose -debug " + dstJarFilename + " a");
+    }
+
+    OutputAnalyzer signB(String jarFilename, String additionalJarsignerOptions,
+            int updateExitCodeVerifyA) throws Exception {
+        jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
+                + (additionalJarsignerOptions == null ? "" :
+                    " " + additionalJarsignerOptions)
+                + " -verbose -debug " + jarFilename + " b");
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename), "signed again with signer b");
+        // check assumption that jar is valid at this point with any alias
+        jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilename);
+        // check assumption that jar is valid at this point with b just signed
+        jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilename + " b");
+        // return result of verification of signature by a before update
+        return jarsignerProc("-verify -strict " + "-keystore " +
+                KEYSTORE_FILENAME + " -storepass changeit " + "-debug " +
+                "-verbose " + jarFilename + " a")
+                .shouldHaveExitValue(updateExitCodeVerifyA);
+    }
+
+    String[] fromFirstToSecondEmptyLine(String[] lines) {
+        int from = 0;
+        for (int i = 0; i < lines.length; i++) {
+            if ("".equals(lines[i])) {
+                from = i + 1;
+                break;
+            }
+        }
+
+        int to = lines.length - 1;
+        for (int i = from; i < lines.length; i++) {
+            if ("".equals(lines[i])) {
+                to = i - 1;
+                break;
+            }
+        }
+
+        return Arrays.copyOfRange(lines, from, to + 1);
+    }
+
+    /**
+     * @see "concise_jarsigner.sh"
+     */
+    String[] getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(
+            String jarFilename, String digestalg,
+            String firstAddedFilename, String secondAddedFilename) {
+        final String TS = ".{28,29}"; // matches a timestamp
+        List<String> expLines = new ArrayList<>();
+        expLines.add("s k   *\\d+ " + TS + " META-INF/MANIFEST[.]MF");
+        expLines.add("      *\\d+ " + TS + " META-INF/B[.]SF");
+        expLines.add("      *\\d+ " + TS + " META-INF/B[.]DSA");
+        expLines.add("      *\\d+ " + TS + " META-INF/A[.]SF");
+        expLines.add("      *\\d+ " + TS + " META-INF/A[.]DSA");
+        if (firstAddedFilename != null) {
+            expLines.add("smk   *\\d+ " + TS + " " + firstAddedFilename);
+        }
+        if (secondAddedFilename != null) {
+            expLines.add("smkX  *\\d+ " + TS + " " + secondAddedFilename);
+        }
+        return expLines.toArray(new String[expLines.size()]);
+    }
+
+    void assertMatchByLines(String[] actLines, String[] expLines) {
+        for (int i = 0; i < actLines.length && i < expLines.length; i++) {
+            String actLine = actLines[i];
+            String expLine = expLines[i];
+            assertTrue(actLine.matches("^" + expLine + "$"),
+                "\"" + actLine + "\" should have matched \"" + expLine + "\"");
+        }
+        assertEquals(actLines.length, expLines.length);
+    }
+
+    String test(String name, Function<Manifest, byte[]> mm) throws Exception {
+        return test(name, FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,
+                mm);
+    }
+
+    String test(String name,
+            String firstAddedFilename, String secondAddedFilename,
+            Function<Manifest, byte[]> mm) throws Exception {
+        return test(name, firstAddedFilename, secondAddedFilename, mm, null,
+                true, true);
+    }
+
+    /**
+     * Essentially, creates a first signed JAR file with a single contained
+     * file or without and a manipulation applied to its manifest signed by
+     * signer a and then signes it again with a different signer b.
+     * The jar file is signed twice with signer a in order to make the digests
+     * available to the manipulation function that might use it.
+     *
+     * @param name Prefix for the JAR filenames used throughout the test.
+     * @param firstAddedFilename Name of a file to add before the first
+     * signature by signer a or null. The name will also become the contents
+     * if not null.
+     * @param secondAddedFilename Name of a file to add after the first
+     * signature by signer a and before the second signature by signer b or
+     * null. The name will also become the contents if not null.
+     * @param manifestManipulation A callback hook to manipulate the manifest
+     * after the first signature by signer a and before the second signature by
+     * signer b.
+     * @param digestalg The digest algorithm name to be used or null for
+     * default.
+     * @param assertMainAttrsDigestsUnchanged Assert that the
+     * manifest main attributes digests have not changed. In any case the test
+     * also checks that the digests are still valid whether changed or not
+     * by {@code jarsigner -verify} which might use
+     * {@link ManifestDigester.Entry#digestWorkaround}
+     * @param assertFirstAddedFileDigestsUnchanged Assert that the
+     * digest of the file firstAddedFilename has not changed with the second
+     * signature. In any case the test checks that the digests are valid whether
+     * changed or not by {@code jarsigner -verify} which might use
+     * {@link ManifestDigester.Entry#digestWorkaround}
+     * @return The name of the resulting JAR file that has passed the common
+     * assertions ready for further examination
+     */
+    String test(String name,
+            String firstAddedFilename, String secondAddedFilename,
+            Function<Manifest, byte[]> manifestManipulation,
+            String digestalg, boolean assertMainAttrsDigestsUnchanged,
+            boolean assertFirstAddedFileDigestsUnchanged)
+                    throws Exception {
+        String digOpts = (digestalg != null ? "-digestalg " + digestalg : "");
+        String jarFilename1 = "test-" + name + "-step1.jar";
+        createSignedJarA(jarFilename1,
+                /* no manifest will let jarsigner create a default one */ null,
+                digOpts, firstAddedFilename);
+
+        // manipulate the manifest, write it back, and sign the jar again with
+        // the same certificate a as before overwriting the first signature
+        String jarFilename2 = "test-" + name + "-step2.jar";
+        String jarFilename3 = "test-" + name + "-step3.jar";
+        manipulateManifestSignAgainA(jarFilename1, jarFilename2, jarFilename3,
+                digOpts, manifestManipulation);
+
+        // add another file, sign it with the other certificate, and verify it
+        String jarFilename4 = "test-" + name + "-step4.jar";
+        JarUtils.updateJar(jarFilename3, jarFilename4,
+                secondAddedFilename != null ?
+                Map.of(secondAddedFilename, secondAddedFilename)
+                : Collections.EMPTY_MAP);
+        OutputAnalyzer o = signB(jarFilename4, digOpts,
+           secondAddedFilename != null ? NOTSIGNEDBYALIASORALIASNOTINSTORE : 0);
+        // check that secondAddedFilename is the only entry which is not signed
+        // by signer with alias "a" unless secondAddedFilename is null
+        assertMatchByLines(
+                fromFirstToSecondEmptyLine(o.getStdout().split("\\R")),
+                getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(
+                        jarFilename4, digestalg,
+                        firstAddedFilename, secondAddedFilename));
+
+        // double-check reading the files with a verifying JarFile
+        try (JarFile jar = new JarFile(jarFilename4, true)) {
+            if (firstAddedFilename != null) {
+                JarEntry je1 = jar.getJarEntry(firstAddedFilename);
+                jar.getInputStream(je1).readAllBytes();
+                assertTrue(je1.getCodeSigners().length > 0);
+            }
+            if (secondAddedFilename != null) {
+                JarEntry je2 = jar.getJarEntry(secondAddedFilename);
+                jar.getInputStream(je2).readAllBytes();
+                assertTrue(je2.getCodeSigners().length > 0);
+            }
+        }
+
+        // assert that the signature of firstAddedFilename signed by signer
+        // with alias "a" is not lost and its digest remains the same
+        try (ZipFile zip = new ZipFile(jarFilename4)) {
+            ZipEntry ea = zip.getEntry("META-INF/A.SF");
+            Manifest sfa = new Manifest(zip.getInputStream(ea));
+            ZipEntry eb = zip.getEntry("META-INF/B.SF");
+            Manifest sfb = new Manifest(zip.getInputStream(eb));
+            if (assertMainAttrsDigestsUnchanged) {
+                String mainAttrsDigKey =
+                        (digestalg != null ? digestalg : "SHA-256") +
+                        "-Digest-Manifest-Main-Attributes";
+                assertEquals(sfa.getMainAttributes().getValue(mainAttrsDigKey),
+                             sfb.getMainAttributes().getValue(mainAttrsDigKey));
+            }
+            if (assertFirstAddedFileDigestsUnchanged) {
+                assertEquals(sfa.getAttributes(firstAddedFilename),
+                             sfb.getAttributes(firstAddedFilename));
+            }
+        }
+
+        return jarFilename4;
+    }
+
+    /**
+     * Test that signing a jar with manifest entries with arbitrary line break
+     * positions in individual section headers does not destroy an existing
+     * signature<ol>
+     * <li>create two self-signed certificates</li>
+     * <li>sign a jar with at least one non-META-INF file in it with a JDK
+     * before 11 or place line breaks not at 72 bytes in an individual section
+     * header</li>
+     * <li>add a new file to the jar</li>
+     * <li>sign the jar with a JDK 11, 12, or 13 with bug 8217375 not yet
+     * resolved with a different signer</li>
+     * </ol>&rarr; first signature will not validate anymore even though it
+     * should.
+     */
+    @Test
+    public void arbitraryLineBreaksSectionName() throws Exception {
+        test("arbitraryLineBreaksSectionName", m -> {
+            return (
+                Name.MANIFEST_VERSION + ": 1.0\r\n" +
+                "Created-By: " +
+                        m.getMainAttributes().getValue("Created-By") + "\r\n" +
+                "\r\n" +
+                "Name: Test\r\n" +
+                " -\r\n" +
+                " Section\r\n" +
+                "Key: Value \r\n" +
+                "\r\n" +
+                "Name: " + FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n" +
+                " " + FILENAME_INITIAL_CONTENTS.substring(1, 8) + "\r\n" +
+                " " + FILENAME_INITIAL_CONTENTS.substring(8) + "\r\n" +
+                "SHA-256-Digest: " + m.getAttributes(FILENAME_INITIAL_CONTENTS)
+                        .getValue("SHA-256-Digest") + "\r\n" +
+                "\r\n"
+            ).getBytes(UTF_8);
+        });
+    }
+
+    /**
+     * Test that signing a jar with manifest entries with arbitrary line break
+     * positions in individual section headers does not destroy an existing
+     * signature<ol>
+     * <li>create two self-signed certificates</li>
+     * <li>sign a jar with at least one non-META-INF file in it with a JDK
+     * before 11 or place line breaks not at 72 bytes in an individual section
+     * header</li>
+     * <li>add a new file to the jar</li>
+     * <li>sign the jar with a JDK 11 or 12 with a different signer</li>
+     * </ol>&rarr; first signature will not validate anymore even though it
+     * should.
+     */
+    @Test
+    public void arbitraryLineBreaksHeader() throws Exception {
+        test("arbitraryLineBreaksHeader", m -> {
+            String digest = m.getAttributes(FILENAME_INITIAL_CONTENTS)
+                    .getValue("SHA-256-Digest");
+            return (
+                Name.MANIFEST_VERSION + ": 1.0\r\n" +
+                "Created-By: " +
+                        m.getMainAttributes().getValue("Created-By") + "\r\n" +
+                "\r\n" +
+                "Name: Test-Section\r\n" +
+                "Key: Value \r\n" +
+                " with\r\n" +
+                "  strange \r\n" +
+                " line breaks.\r\n" +
+                "\r\n" +
+                "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
+                "SHA-256-Digest: " + digest.substring(0, 11) + "\r\n" +
+                " " + digest.substring(11) + "\r\n" +
+                "\r\n"
+            ).getBytes(UTF_8);
+        });
+    }
+
+    /**
+     * Breaks {@code line} at 70 bytes even though the name says 72 but when
+     * also counting the line delimiter ("{@code \r\n}") the line totals to 72
+     * bytes.
+     * Borrowed from {@link Manifest#make72Safe} before JDK 11
+     *
+     * @see Manifest#make72Safe
+     */
+    static void make72Safe(StringBuffer line) {
+        int length = line.length();
+        if (length > 72) {
+            int index = 70;
+            while (index < length - 2) {
+                line.insert(index, "\r\n ");
+                index += 72;
+                length += 3;
+            }
+        }
+        return;
+    }
+
+    /**
+     * Test that signing a jar with manifest entries with line breaks at
+     * position where Manifest would not place them now anymore (72 instead of
+     * 70 bytes after line starts) does not destroy an existing signature<ol>
+     * <li>create two self-signed certificates</li>
+     * <li>simulate a manifest as it would have been written by a JDK before 11
+     * by re-positioning line breaks at 70 bytes (which makes a difference by
+     * digests that grow headers longer than 70 characters such as SHA-512 as
+     * opposed to default SHA-256, long file names, or manual editing)</li>
+     * <li>add a new file to the jar</li>
+     * <li>sign the jar with a JDK 11 or 12 with a different signer</li>
+     * </ol><p>&rarr;
+     * The first signature will not validate anymore even though it should.
+     */
+    public void lineWidth70(String name, String digestalg) throws Exception {
+        Files.write(Path.of(name), name.getBytes(UTF_8));
+        test(name, name, FILENAME_UPDATED_CONTENTS, m -> {
+            // force a line break with a header exceeding line width limit
+            m.getEntries().put("Test-Section", new Attributes());
+            m.getAttributes("Test-Section").put(
+                    Name.IMPLEMENTATION_VERSION, "1" + "0".repeat(100));
+
+            StringBuilder sb = new StringBuilder();
+            StringBuffer[] buf = new StringBuffer[] { null };
+            manifestToString(m).lines().forEach(line -> {
+                if (line.startsWith(" ")) {
+                    buf[0].append(line.substring(1));
+                } else {
+                    if (buf[0] != null) {
+                        make72Safe(buf[0]);
+                        sb.append(buf[0].toString());
+                        sb.append("\r\n");
+                    }
+                    buf[0] = new StringBuffer();
+                    buf[0].append(line);
+                }
+            });
+            make72Safe(buf[0]);
+            sb.append(buf[0].toString());
+            sb.append("\r\n");
+            return sb.toString().getBytes(UTF_8);
+        }, digestalg, false, false);
+    }
+
+    @Test
+    public void lineWidth70Filename() throws Exception {
+        lineWidth70(
+            "lineWidth70".repeat(6) /* 73 chars total with "Name: " */, null);
+    }
+
+    @Test
+    public void lineWidth70Digest() throws Exception {
+        lineWidth70("lineWidth70digest", "SHA-512");
+    }
+
+    /**
+     * Test that signing a jar with a manifest with line delimiter other than
+     * "{@code \r\n}" does not destroy an existing signature<ol>
+     * <li>create two self-signed certificates</li>
+     * <li>sign a jar with at least one non-META-INF file in it</li>
+     * <li>extract the manifest, and change its line delimiters
+     * (for example dos2unix)</li>
+     * <li>update the jar with the updated manifest</li>
+     * <li>sign it again with the same signer as before</li>
+     * <li>add a new file to the jar</li>
+     * <li>sign the jar with a JDK before 13 with a different signer<li>
+     * </ol><p>&rarr;
+     * The first signature will not validate anymore even though it should.
+     */
+    public void lineBreak(String lineBreak) throws Exception {
+        test("lineBreak" + byteArrayToIntList(lineBreak.getBytes(UTF_8)).stream
+                ().map(i -> "" + i).collect(Collectors.joining("")), m -> {
+            StringBuilder sb = new StringBuilder();
+            manifestToString(m).lines().forEach(l -> {
+                sb.append(l);
+                sb.append(lineBreak);
+            });
+            return sb.toString().getBytes(UTF_8);
+        });
+    }
+
+    @Test
+    public void lineBreakCr() throws Exception {
+        lineBreak("\r");
+    }
+
+    @Test
+    public void lineBreakLf() throws Exception {
+        lineBreak("\n");
+    }
+
+    @Test
+    public void lineBreakCrLf() throws Exception {
+        lineBreak("\r\n");
+    }
+
+    @Test
+    public void testAdjacentRepeatedSection() throws Exception {
+        test("adjacent", m -> {
+            return (manifestToString(m) +
+                    "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
+                    "Foo: Bar\r\n" +
+                    "\r\n"
+            ).getBytes(UTF_8);
+        });
+    }
+
+    @Test
+    public void testIntermittentRepeatedSection() throws Exception {
+        test("intermittent", m -> {
+            return (manifestToString(m) +
+                    "Name: don't know\r\n" +
+                    "Foo: Bar\r\n" +
+                    "\r\n" +
+                    "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
+                    "Foo: Bar\r\n" +
+                    "\r\n"
+            ).getBytes(UTF_8);
+        });
+    }
+
+    @Test
+    public void testNameImmediatelyContinued() throws Exception {
+        test("testNameImmediatelyContinued", m -> {
+            // places a continuation line break and space at the first allowed
+            // position after ": " and before the first character of the value
+            return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
+                    "\r\n " + FILENAME_INITIAL_CONTENTS + "\r\nFoo: Bar")
+            ).getBytes(UTF_8);
+        });
+    }
+
+    /*
+     * "malicious" '\r' after continuation line continued
+     */
+    @Test
+    public void testNameContinuedContinuedWithCr() throws Exception {
+        test("testNameContinuedContinuedWithCr", m -> {
+            return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
+                    FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +
+                    FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r " +
+                    FILENAME_INITIAL_CONTENTS.substring(4) + "\r\n" +
+                    "Foo: Bar")
+            ).getBytes(UTF_8);
+        });
+    }
+
+    /*
+     * "malicious" '\r' after continued continuation line
+     */
+    @Test
+    public void testNameContinuedContinuedEndingWithCr() throws Exception {
+        test("testNameContinuedContinuedEndingWithCr", m -> {
+            return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
+                    FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +
+                    FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r\n " +
+                    FILENAME_INITIAL_CONTENTS.substring(4) + "\r" + // no '\n'
+                    "Foo: Bar")
+            ).getBytes(UTF_8);
+        });
+    }
+
+    @DataProvider(name = "trailingSeqParams", parallel = true)
+    public static Object[][] trailingSeqParams() {
+        return new Object[][] {
+            {""},
+            {"\r"},
+            {"\n"},
+            {"\r\n"},
+            {"\r\r"},
+            {"\n\n"},
+            {"\n\r"},
+            {"\r\r\r"},
+            {"\r\r\n"},
+            {"\r\n\r"},
+            {"\r\n\n"},
+            {"\n\r\r"},
+            {"\n\r\n"},
+            {"\n\n\r"},
+            {"\n\n\n"},
+            {"\r\r\r\n"},
+            {"\r\r\n\r"},
+            {"\r\r\n\n"},
+            {"\r\n\r\r"},
+            {"\r\n\r\n"},
+            {"\r\n\n\r"},
+            {"\r\n\n\n"},
+            {"\n\r\r\n"},
+            {"\n\r\n\r"},
+            {"\n\r\n\n"},
+            {"\n\n\r\n"},
+            {"\r\r\n\r\n"},
+            {"\r\n\r\r\n"},
+            {"\r\n\r\n\r"},
+            {"\r\n\r\n\n"},
+            {"\r\n\n\r\n"},
+            {"\n\r\n\r\n"},
+            {"\r\n\r\n\r\n"},
+            {"\r\n\r\n\r\n\r\n"}
+        };
+    }
+
+    boolean isSufficientSectionDelimiter(String trailingSeq) {
+        if (trailingSeq.length() < 2) return false;
+        if (trailingSeq.startsWith("\r\n")) {
+            trailingSeq = trailingSeq.substring(2);
+        } else if (trailingSeq.startsWith("\r") ||
+                   trailingSeq.startsWith("\n")) {
+            trailingSeq = trailingSeq.substring(1);
+        } else {
+            return false;
+        }
+        if (trailingSeq.startsWith("\r\n")) {
+            return true;
+        } else if (trailingSeq.startsWith("\r") ||
+                trailingSeq.startsWith("\n")) {
+            return true;
+        }
+        return false;
+    }
+
+    Function<Manifest, byte[]> replaceTrailingLineBreaksManipulation(
+            String trailingSeq) {
+        return m -> {
+            StringBuilder sb = new StringBuilder(manifestToString(m));
+            // cut off default trailing line break characters
+            while ("\r\n".contains(sb.substring(sb.length() - 1))) {
+                sb.deleteCharAt(sb.length() - 1);
+            }
+            // and instead add another trailing sequence
+            sb.append(trailingSeq);
+            return sb.toString().getBytes(UTF_8);
+        };
+    }
+
+    boolean abSigFilesEqual(String jarFilename,
+            Function<Manifest,Object> getter) throws IOException {
+        try (ZipFile zip = new ZipFile(jarFilename)) {
+            ZipEntry ea = zip.getEntry("META-INF/A.SF");
+            Manifest sfa = new Manifest(zip.getInputStream(ea));
+            ZipEntry eb = zip.getEntry("META-INF/B.SF");
+            Manifest sfb = new Manifest(zip.getInputStream(eb));
+            return getter.apply(sfa).equals(getter.apply(sfb));
+        }
+    }
+
+    /**
+     * Create a signed JAR file with a strange sequence of line breaks after
+     * the main attributes and no individual section and hence no file contained
+     * within the JAR file in order not to produce an individual section,
+     * then add no other file and sign it with a different signer.
+     * The manifest is not expected to be changed during the second signature.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void emptyJarTrailingSeq(String trailingSeq) throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        if (trailingSeq.isEmpty()) {
+            return; // invalid manifest without trailing line break
+        }
+
+        test("emptyJarTrailingSeq" + trailingSeqEscaped, null, null,
+                replaceTrailingLineBreaksManipulation(trailingSeq));
+
+        // test called above already asserts by default that the main attributes
+        // digests have not changed.
+    }
+
+    /**
+     * Create a signed JAR file with a strange sequence of line breaks after
+     * the main attributes and no individual section and hence no file contained
+     * within the JAR file in order not to produce an individual section,
+     * then add another file and sign it with a different signer so that the
+     * originally trailing sequence after the main attributes might have to be
+     * completed to a full section delimiter or reproduced only partially
+     * before the new individual section with the added file digest can be
+     * appended. The main attributes digests are expected to change if the
+     * first signed trailing sequence did not contain a blank line and are not
+     * expected to change if superfluous parts of the trailing sequence were
+     * not reproduced. All digests are expected to validate either with digest
+     * or with digestWorkaround.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void emptyJarTrailingSeqAddFile(String trailingSeq) throws Exception{
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        if (!isSufficientSectionDelimiter(trailingSeq)) {
+            return; // invalid manifest without trailing blank line
+        }
+        boolean expectUnchangedDigests =
+                isSufficientSectionDelimiter(trailingSeq);
+        System.out.println("expectUnchangedDigests = " + expectUnchangedDigests);
+        String jarFilename = test("emptyJarTrailingSeqAddFile" +
+                trailingSeqEscaped, null, FILENAME_UPDATED_CONTENTS,
+                replaceTrailingLineBreaksManipulation(trailingSeq),
+                null, expectUnchangedDigests, false);
+
+        // Check that the digests have changed only if another line break had
+        // to be added before a new individual section. That both also are valid
+        // with either digest or digestWorkaround has been checked by test
+        // before.
+        assertEquals(abSigFilesEqual(jarFilename, sf -> sf.getMainAttributes()
+                        .getValue("SHA-256-Digest-Manifest-Main-Attributes")),
+                     expectUnchangedDigests);
+    }
+
+    /**
+     * Create a signed JAR file with a strange sequence of line breaks after
+     * the only individual section holding the digest of the only file contained
+     * within the JAR file,
+     * then add no other file and sign it with a different signer.
+     * The manifest is expected to be changed during the second signature only
+     * by removing superfluous line break characters which are not digested
+     * and the manifest entry digest is expected not to change.
+     * The individual section is expected to be reproduced without additional
+     * line breaks even if the trailing sequence does not properly delimit
+     * another section.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void singleIndividualSectionTrailingSeq(String trailingSeq)
+            throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        if (trailingSeq.isEmpty()) {
+            return; // invalid manifest without trailing line break
+        }
+        String jarFilename = test("singleIndividualSectionTrailingSeq"
+                + trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null,
+                replaceTrailingLineBreaksManipulation(trailingSeq));
+
+        assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+                FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+    }
+
+    /**
+     * Create a signed JAR file with a strange sequence of line breaks after
+     * the first individual section holding the digest of the only file
+     * contained within the JAR file and a second individual section with the
+     * same name to be both digested into the same entry digest,
+     * then add no other file and sign it with a different signer.
+     * The manifest is expected to be changed during the second signature
+     * by removing superfluous line break characters which are not digested
+     * anyway or if the trailingSeq is not a sufficient delimiter that both
+     * intially provided sections are treated as only one which is maybe not
+     * perfect but does at least not result in an invalid signed jar file.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void firstIndividualSectionTrailingSeq(String trailingSeq)
+            throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        String jarFilename;
+        jarFilename =  test("firstIndividualSectionTrailingSeq"
+                + trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {
+                StringBuilder sb = new StringBuilder(manifestToString(m));
+                // cut off default trailing line break characters
+                while ("\r\n".contains(sb.substring(sb.length() - 1))) {
+                    sb.deleteCharAt(sb.length() - 1);
+                }
+                // and instead add another trailing sequence
+                sb.append(trailingSeq);
+                // now add another section with the same name assuming sb
+                // already contains one entry for FILENAME_INITIAL_CONTENTS
+                sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+                sb.append("Foo: Bar\r\n");
+                sb.append("\r\n");
+                return sb.toString().getBytes(UTF_8);
+        });
+
+        assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+                FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+    }
+
+    /**
+     * Create a signed JAR file with two individual sections for the same
+     * contained file (corresponding by name) the first of which properly
+     * delimited and the second of which followed by a strange sequence of
+     * line breaks both digested into the same entry digest,
+     * then add no other file and sign it with a different signer.
+     * The manifest is expected to be changed during the second signature
+     * by removing superfluous line break characters which are not digested
+     * anyway.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void secondIndividualSectionTrailingSeq(String trailingSeq)
+            throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        String jarFilename = test("secondIndividualSectionTrailingSeq" +
+                trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {
+            StringBuilder sb = new StringBuilder(manifestToString(m));
+            sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+            sb.append("Foo: Bar");
+            sb.append(trailingSeq);
+            return sb.toString().getBytes(UTF_8);
+        });
+
+        assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+                FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+    }
+
+    /**
+     * Create a signed JAR file with a strange sequence of line breaks after
+     * the only individual section holding the digest of the only file contained
+     * within the JAR file,
+     * then add another file and sign it with a different signer.
+     * The manifest is expected to be changed during the second signature by
+     * removing superfluous line break characters which are not digested
+     * anyway or adding another line break to complete to a proper section
+     * delimiter blank line.
+     * The first file entry digest is expected to change only if another
+     * line break has been added.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void singleIndividualSectionTrailingSeqAddFile(String trailingSeq)
+            throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        if (!isSufficientSectionDelimiter(trailingSeq)) {
+            return; // invalid manifest without trailing blank line
+        }
+        String jarFilename = test("singleIndividualSectionTrailingSeqAddFile"
+                + trailingSeqEscaped,
+                FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,
+                replaceTrailingLineBreaksManipulation(trailingSeq),
+                null, true, true);
+
+        assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+                        FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+    }
+
+    /**
+     * Create a signed JAR file with a strange sequence of line breaks after
+     * the first individual section holding the digest of the only file
+     * contained within the JAR file and a second individual section with the
+     * same name to be both digested into the same entry digest,
+     * then add another file and sign it with a different signer.
+     * The manifest is expected to be changed during the second signature
+     * by removing superfluous line break characters which are not digested
+     * anyway or if the trailingSeq is not a sufficient delimiter that both
+     * intially provided sections are treated as only one which is maybe not
+     * perfect but does at least not result in an invalid signed jar file.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void firstIndividualSectionTrailingSeqAddFile(String trailingSeq)
+            throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        String jarFilename =  test("firstIndividualSectionTrailingSeqAddFile"
+                + trailingSeqEscaped,
+                FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {
+                StringBuilder sb = new StringBuilder(manifestToString(m));
+                // cut off default trailing line break characters
+                while ("\r\n".contains(sb.substring(sb.length() - 1))) {
+                    sb.deleteCharAt(sb.length() - 1);
+                }
+                // and instead add another trailing sequence
+                sb.append(trailingSeq);
+                // now add another section with the same name assuming sb
+                // already contains one entry for FILENAME_INITIAL_CONTENTS
+                sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+                sb.append("Foo: Bar\r\n");
+                sb.append("\r\n");
+                return sb.toString().getBytes(UTF_8);
+        });
+
+        assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+                FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+    }
+
+    /**
+     * Create a signed JAR file with two individual sections for the same
+     * contained file (corresponding by name) the first of which properly
+     * delimited and the second of which followed by a strange sequence of
+     * line breaks both digested into the same entry digest,
+     * then add another file and sign it with a different signer.
+     * The manifest is expected to be changed during the second signature
+     * by removing superfluous line break characters which are not digested
+     * anyway or by adding a proper section delimiter.
+     * The digests are expected to be changed only if another line break is
+     * added to properly delimit the next section both digests of which are
+     * expected to validate with either digest or digestWorkaround.
+     */
+    @Test(dataProvider = "trailingSeqParams")
+    public void secondIndividualSectionTrailingSeqAddFile(String trailingSeq)
+            throws Exception {
+        String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+              UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+        System.out.println("trailingSeq = " + trailingSeqEscaped);
+        if (!isSufficientSectionDelimiter(trailingSeq)) {
+            return; // invalid manifest without trailing blank line
+        }
+        String jarFilename = test("secondIndividualSectionTrailingSeqAddFile" +
+                trailingSeqEscaped,
+                FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {
+            StringBuilder sb = new StringBuilder(manifestToString(m));
+            sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+            sb.append("Foo: Bar");
+            sb.append(trailingSeq);
+            return sb.toString().getBytes(UTF_8);
+        }, null, true, true);
+
+        assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+                FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+    }
+
+    String manifestToString(Manifest mf) {
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            mf.write(out);
+            return new String(out.toByteArray(), UTF_8);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static List<Integer> byteArrayToIntList(byte[] bytes) {
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < bytes.length; i++) {
+            list.add((int) bytes[i]);
+        }
+        return list;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/RemoveDifferentKeyAlgBlockFile.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.nio.file.Path;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes.Name;
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @run testng RemoveDifferentKeyAlgBlockFile
+ * @summary Checks that if a signed jar file is signed again with the same
+ * signer name and a different algorithm that the signature block file for
+ * the previous signature is removed. Example: the jar had META-INF/A.SF and
+ * META-INF/A.RSA files and is now signed with DSA. So it should contain
+ * an updated META-INF/A.SF and META-INF/A.DSA and the META-INF/A.RSA should
+ * be removed because not valid any longer.
+ */
+public class RemoveDifferentKeyAlgBlockFile {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    @BeforeClass
+    public void prepareCertificates() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg RSA -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias RSA -dname CN=RSA").shouldHaveExitValue(0);
+        SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias DSA -dname CN=DSA").shouldHaveExitValue(0);
+    }
+
+    @Test
+    public void testOtherAlgSigBlockFileRemoved() throws Exception {
+        String jarFilename = "test.jar";
+        JarUtils.createJarFile(Path.of(jarFilename), (Manifest) null,
+                Path.of("."));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug -sigfile A " +
+                jarFilename + " RSA").shouldHaveExitValue(0);
+
+        // change the jar file to invalidate the first signature with RSA
+        String jarFilenameModified = "modified.jar";
+        try (JarFile jar = new JarFile(jarFilename)) {
+            Manifest manifest = jar.getManifest();
+            manifest.getMainAttributes().put(
+                    new Name("Some-Key"), "Some-Value");
+            JarUtils.updateManifest(jarFilename, jarFilenameModified, manifest);
+        }
+
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug -sigfile A " +
+                jarFilenameModified + " DSA").shouldHaveExitValue(0);
+        SecurityTools.jarsigner("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilenameModified)
+                .shouldHaveExitValue(0);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/SectionNameContinuedVsLineBreak.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.jar.Manifest;
+import java.util.jar.JarFile;
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @run testng SectionNameContinuedVsLineBreak
+ * @summary Checks some specific line break character sequences in section name
+ * continuation line breaks.
+ */
+public class SectionNameContinuedVsLineBreak {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    @BeforeTest
+    public void prepareCertificate() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg EC -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=A").shouldHaveExitValue(0);
+    }
+
+    void manipulateManifestSignAgainA(
+            String srcJarFilename, String dstJarFilename,
+            Function<Manifest, byte[]> manifestManipulation) throws Exception {
+        byte[] manipulatedManifest = manifestManipulation.apply(
+                new Manifest(new ByteArrayInputStream(
+                        Utils.readJarManifestBytes(srcJarFilename))));
+        Utils.echoManifest(manipulatedManifest, "manipulated manifest");
+        JarUtils.updateJar(srcJarFilename, dstJarFilename, Map.of(
+                JarFile.MANIFEST_NAME, manipulatedManifest));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + dstJarFilename + " a")
+                .shouldHaveExitValue(0);
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                dstJarFilename), "manipulated jar signed again with a");
+        // check assumption that jar is valid at this point
+        SecurityTools.jarsigner("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + dstJarFilename + " a")
+                .shouldHaveExitValue(0);
+    }
+
+    void test(String name, Function<Manifest, byte[]> manifestManipulation,
+            String jarContentFilename) throws Exception {
+        String jarFilename1 = "test-" + name + "-step1.jar";
+        Files.write(Path.of(jarContentFilename),
+                jarContentFilename.getBytes(UTF_8));
+        JarUtils.createJarFile(Path.of(jarFilename1), (Manifest)
+                /* no manifest will let jarsigner create a default one */ null,
+                Path.of("."), Path.of(jarContentFilename));
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + jarFilename1 +
+                " a").shouldHaveExitValue(0);
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename1), "signed jar");
+        String jarFilename2 = "test-" + name + "-step2.jar";
+        manipulateManifestSignAgainA(jarFilename1, jarFilename2,
+                manifestManipulation);
+
+        SecurityTools.jarsigner("-verify -strict -keystore " +
+                KEYSTORE_FILENAME + " -storepass changeit -debug -verbose " +
+                jarFilename2 + " a").shouldHaveExitValue(0);
+    }
+
+    /**
+     * Test signing a jar with a manifest that has an entry the name of
+     * which continued on a continuation line with '\r' as line break before
+     * the continuation line space ' ' on the line the name starts.
+     */
+    @Test
+    public void testContinueNameAfterCr() throws Exception {
+        String filename = "abc";
+        test("testContinueNameAfterCr", m -> {
+            String digest = m.getAttributes("abc").getValue("SHA-256-Digest");
+            m.getEntries().remove("abc");
+            return (manifestToString(m)
+                    + "Name: a\r"
+                    + " bc\r\n"
+                    + "SHA-256-Digest: " + digest + "\r\n"
+                    + "\r\n").getBytes(UTF_8);
+        }, filename);
+    }
+
+    /**
+     * Test signing a jar with a manifest that has an entry the name of
+     * which continued on a continuation line with '\r' as line break before
+     * the continuation line space ' ' after a first continuation.
+     */
+    @Test
+    public void testContinueNameAfterCrOnContinuationLine() throws Exception {
+        String filename = "abc";
+        test("testContinueNameAfterCr", m -> {
+            String digest = m.getAttributes("abc").getValue("SHA-256-Digest");
+            m.getEntries().remove("abc");
+            return (manifestToString(m)
+                    + "Name: a\r\n"
+                    + " b\r"
+                    + " c\r\n"
+                    + "SHA-256-Digest: " + digest + "\r\n"
+                    + "\r\n").getBytes(UTF_8);
+        }, filename);
+    }
+
+    /**
+     * Test signing a jar with a manifest that has an entry the name of
+     * which continued on a continuation line and terminated with '\r' as line
+     * break after the name.
+     */
+    @Test
+    public void testEndNameWithCrOnContinuationLine() throws Exception {
+        String filename = "abc";
+        test("testContinueNameAfterCr", m -> {
+            String digest = m.getAttributes("abc").getValue("SHA-256-Digest");
+            m.getEntries().remove("abc");
+            return (manifestToString(m)
+                    + "Name: a\r\n"
+                    + " bc\r"
+                    + "SHA-256-Digest: " + digest + "\r\n"
+                    + "\r\n").getBytes(UTF_8);
+        }, filename);
+    }
+
+    String manifestToString(Manifest mf) {
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            mf.write(out);
+            return out.toString(UTF_8);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
--- a/test/jdk/sun/security/tools/jarsigner/Utils.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/Utils.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -21,8 +21,15 @@
  * questions.
  */
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.InputStream;
 import java.io.IOException;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipFile;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 /**
  * Helper class.
@@ -35,5 +42,66 @@
         }
     }
 
+    static void printNonPrintableCharactersEscaped(byte[] manifest)
+            throws IOException {
+        // keep byte sequences encoding multi-byte UTF-8 encoded and composite
+        // characters together as much as possible before decoding into a String
+        ByteArrayOutputStream lineBuf = new ByteArrayOutputStream();
+        for (int i = 0; i < manifest.length; i++) {
+            switch (manifest[i]) {
+            case '\t':
+                lineBuf.write("\\t".getBytes(UTF_8));
+                break;
+            case '\r':
+                lineBuf.write("\\r".getBytes(UTF_8));
+                if (i + 1 >= manifest.length || manifest[i + 1] != '\n') {
+                    System.out.println(lineBuf.toString(UTF_8));
+                    lineBuf.reset();
+                }
+                break;
+            case '\n':
+                lineBuf.write("\\n".getBytes(UTF_8));
+                System.out.println(lineBuf.toString(UTF_8));
+                lineBuf.reset();
+                break;
+            default:
+                lineBuf.write(manifest[i]);
+            }
+        }
+        if (lineBuf.size() > 0) {
+            System.out.println(lineBuf.toString(UTF_8));
+        }
+    }
+
+    static void echoManifest(byte[] manifest, String msg) throws IOException {
+        System.out.println("-".repeat(72));
+        System.out.println(msg);
+        System.out.println("-".repeat(72));
+        printNonPrintableCharactersEscaped(manifest);
+        System.out.println("-".repeat(72));
+    }
+
+    static byte[] readJarManifestBytes(String jarFilename) throws IOException {
+        return readJarEntryBytes(jarFilename, JarFile.MANIFEST_NAME);
+    }
+
+    static byte[] readJarEntryBytes(String jarFilename, String jarEntryname)
+            throws IOException {
+        try (
+            ZipFile jar = new ZipFile(jarFilename);
+            InputStream is = jar.getInputStream(jar.getEntry(jarEntryname));
+        ) {
+            return is.readAllBytes();
+        }
+    }
+
+    static String escapeStringWithNumbers(String lineBreak) {
+        String escaped = "";
+        byte[] bytes = lineBreak.getBytes(UTF_8);
+        for (int i = 0; i < bytes.length; i++) {
+            escaped += bytes[i];
+        }
+        return escaped;
+    }
+
 }
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/WasSignedByOtherSigner.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes.Name;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import jdk.test.lib.util.JarUtils;
+import jdk.test.lib.SecurityTools;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @library /test/lib
+ * @run testng WasSignedByOtherSigner
+ * @summary Checks that {@code wasSigned} in
+ * {@link jdk.security.jarsigner.JarSigner#sign0} is set true if the jar to sign
+ * contains a signature that will not be overwritten with the current one.
+ */
+public class WasSignedByOtherSigner {
+
+    static final String KEYSTORE_FILENAME = "test.jks";
+
+    @BeforeClass
+    public void prepareKeyStore() throws Exception {
+        SecurityTools.keytool("-genkeypair -keyalg EC -keystore "
+                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
+                + " -alias a -dname CN=A").shouldHaveExitValue(0);
+    }
+
+    void test(String secondSigner, boolean expRrewritten) throws Exception {
+        String jarFilename1 = "test" + secondSigner + "-1.jar";
+        JarUtils.createJarFile(Path.of(jarFilename1), (Manifest) null,
+                Path.of("."));
+        // TODO: use jarsigner here only to create a default manifest...
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + jarFilename1 + " a")
+                .shouldHaveExitValue(0);
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename1), "initial manifest");
+
+        // replace manifest with a non-standard one that can later be checked
+        String jarFilename2 = "test" + secondSigner + "-2.jar";
+        JarUtils.updateJar(jarFilename1, jarFilename2, Map.of(
+                // add a fake sig-related file to trigger wasSigned in JarSigner
+                "META-INF/.SF", Name.SIGNATURE_VERSION + ": 1.0\r\n"));
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename2), "with fake META-INF.SF file");
+        String jarFilename3 = "test" + secondSigner + "-3.jar";
+        JarUtils.updateManifest(jarFilename2, jarFilename3, new Manifest() {
+            @Override public void write(OutputStream out) throws IOException {
+                // no trailing blank line
+                out.write((Name.MANIFEST_VERSION + ": 1.0\r\n").getBytes(UTF_8));
+            }
+        });
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename3), "with manifest manipulated");
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug " + jarFilename3 + " a")
+                .shouldHaveExitValue(0);
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename3), "signed");
+        String jarFilename4 = "test" + secondSigner + "-4.jar";
+        JarUtils.updateJar(jarFilename3, jarFilename4,
+                Map.of("META-INF/.SF", false));
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename4), "with fake META-INF.SF file removed");
+
+        // re-sign the jar with signer named secondSigner (same or different)
+        SecurityTools.jarsigner("-keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -verbose -debug -sigfile " +
+                secondSigner + " " + jarFilename4 + " a")
+                .shouldHaveExitValue(0);
+        Utils.echoManifest(Utils.readJarManifestBytes(
+                jarFilename4), "signed again");
+        // remove META-INF/.SF from signed jar again which would not validate
+
+        // in any case verify that the resulting jar file is valid
+        SecurityTools.jarsigner("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilename4)
+                .shouldHaveExitValue(0);
+        SecurityTools.jarsigner("-verify -keystore " + KEYSTORE_FILENAME +
+                " -storepass changeit -debug -verbose " + jarFilename4 +
+                " a").shouldHaveExitValue(0);
+
+        // if wasSigned was true in JarSigner#sign0 the manifest (only main
+        // attributes present and tested here but same consideration applies
+        // to individual sections just the same) should be reproduced with
+        // unchanged binary form. Otherwise, if there were no previous
+        // signatures or only one being replaced, the manifest is kind of
+        // "normalized" by re-writing it thereby replacing all line breaks
+        // (from cr or lf to crlf) and replacing all line breaks onto
+        // continuation lines and also writing all section delimiting blank
+        // lines.
+        // if that "normalization" has took place the test here can conclude
+        // whether wasSigned was true or was not.
+        try (ZipFile jar = new ZipFile(jarFilename4)) {
+            ZipEntry ze = jar.getEntry(JarFile.MANIFEST_NAME);
+            byte[] manifestBytes = jar.getInputStream(ze).readAllBytes();
+            Utils.echoManifest(manifestBytes, "manifest");
+            String manifestString = new String(manifestBytes, UTF_8);
+            boolean actRewritten = manifestString.endsWith("\r\n\r\n");
+            assertEquals(actRewritten, expRrewritten);
+        }
+    }
+
+    @Test
+    public void reSignSameSigner() throws Exception {
+        test("A", true);
+    }
+
+    @Test
+    public void reSignOtherSigner() throws Exception {
+        test("B", false);
+    }
+
+}
--- a/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,55 +23,59 @@
 
 /*
  * @test
- * @summary This test is used to verify the compatibility on jarsigner cross
+ * @bug 8217375
+ * @summary This test is used to verify the compatibility of jarsigner across
  *     different JDK releases. It also can be used to check jar signing (w/
- *     and w/o TSA) and verifying on some specific key algorithms and digest
- *     algorithms.
- *     Note that, this is a manual test. For more details about the test and
- *     its usages, please look through README.
+ *     and w/o TSA) and to verify some specific signing and digest algorithms.
+ *     Note that this is a manual test. For more details about the test and
+ *     its usages, please look through the README.
  *
- * @modules java.base/sun.security.pkcs
- *          java.base/sun.security.timestamp
- *          java.base/sun.security.tools.keytool
- *          java.base/sun.security.util
- *          java.base/sun.security.x509
- * @library /test/lib /lib/testlibrary ../warnings
+ * @library /test/lib ../warnings
  * @compile -source 1.7 -target 1.7 JdkUtils.java
  * @run main/manual/othervm Compatibility
  */
 
 import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintStream;
+import java.nio.file.Path;
+import java.nio.file.Files;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes.Name;
 import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import jdk.test.lib.process.OutputAnalyzer;
 import jdk.test.lib.process.ProcessTools;
 import jdk.test.lib.util.JarUtils;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 public class Compatibility {
 
-    private static final String TEST_JAR_NAME = "test.jar";
-
     private static final String TEST_SRC = System.getProperty("test.src");
     private static final String TEST_CLASSES = System.getProperty("test.classes");
     private static final String TEST_JDK = System.getProperty("test.jdk");
-    private static final String TEST_JARSIGNER = jarsignerPath(TEST_JDK);
+    private static JdkInfo TEST_JDK_INFO;
 
     private static final String PROXY_HOST = System.getProperty("proxyHost");
     private static final String PROXY_PORT = System.getProperty("proxyPort", "80");
@@ -84,29 +88,43 @@
             "javaSecurityFile", TEST_SRC + "/java.security");
 
     private static final String PASSWORD = "testpass";
-    private static final String KEYSTORE = "testKeystore";
+    private static final String KEYSTORE = "testKeystore.jks";
 
     private static final String RSA = "RSA";
     private static final String DSA = "DSA";
     private static final String EC = "EC";
-    private static final String[] KEY_ALGORITHMS = new String[] {
+    private static String[] KEY_ALGORITHMS;
+    private static final String[] DEFAULT_KEY_ALGORITHMS = new String[] {
             RSA,
             DSA,
             EC};
 
     private static final String SHA1 = "SHA-1";
     private static final String SHA256 = "SHA-256";
+    private static final String SHA384 = "SHA-384";
     private static final String SHA512 = "SHA-512";
     private static final String DEFAULT = "DEFAULT";
-    private static final String[] DIGEST_ALGORITHMS = new String[] {
+    private static String[] DIGEST_ALGORITHMS;
+    private static final String[] DEFAULT_DIGEST_ALGORITHMS = new String[] {
             SHA1,
             SHA256,
-            SHA512,
+            SHA384,
+            SHA512, // note: digests break onto continuation line in manifest
             DEFAULT};
 
-    private static final boolean[] EXPIRED = new boolean[] {
-            false,
-            true};
+    private static final boolean[] EXPIRED =
+            Boolean.valueOf(System.getProperty("expired", "true")) ?
+                    new boolean[] { false, true } : new boolean[] { false };
+
+    private static final boolean TEST_COMPREHENSIVE_JAR_CONTENTS =
+            Boolean.valueOf(System.getProperty(
+                    "testComprehensiveJarContents", "false"));
+
+    private static final boolean TEST_JAR_UPDATE =
+            Boolean.valueOf(System.getProperty("testJarUpdate", "false"));
+
+    private static final boolean STRICT =
+            Boolean.valueOf(System.getProperty("strict", "false"));
 
     private static final Calendar CALENDAR = Calendar.getInstance();
     private static final DateFormat DATE_FORMAT
@@ -119,7 +137,7 @@
     static {
         if (CERT_VALIDITY < 1 || CERT_VALIDITY > 1440) {
             throw new RuntimeException(
-                    "certValidity if out of range [1, 1440]: " + CERT_VALIDITY);
+                    "certValidity out of range [1, 1440]: " + CERT_VALIDITY);
         }
     }
 
@@ -132,27 +150,34 @@
 
     private static DetailsOutputStream detailsOutput;
 
-    public static void main(String[] args) throws Throwable {
+    private static int sigfileCounter;
+
+    private static String nextSigfileName(String alias, String u, String s) {
+        String sigfileName = "" + (++sigfileCounter);
+        System.out.println("using sigfile " + sigfileName + " for alias "
+                    + alias + " signing " + u + ".jar to " + s + ".jar");
+        return sigfileName;
+    }
+
+    public static void main(String... args) throws Throwable {
         // Backups stdout and stderr.
         PrintStream origStdOut = System.out;
         PrintStream origStdErr = System.err;
 
-        detailsOutput = new DetailsOutputStream();
+        detailsOutput = new DetailsOutputStream(outfile());
 
         // Redirects the system output to a custom one.
         PrintStream printStream = new PrintStream(detailsOutput);
         System.setOut(printStream);
         System.setErr(printStream);
 
+        TEST_JDK_INFO = new JdkInfo(TEST_JDK);
+
         List<TsaInfo> tsaList = tsaInfoList();
-        if (tsaList.size() == 0) {
-            throw new RuntimeException("TSA service is mandatory.");
-        }
-
         List<JdkInfo> jdkInfoList = jdkInfoList();
         List<CertInfo> certList = createCertificates(jdkInfoList);
-        createJar();
-        List<SignItem> signItems = test(jdkInfoList, tsaList, certList);
+        List<SignItem> signItems =
+                test(jdkInfoList, tsaList, certList, createJars());
 
         boolean failed = generateReport(tsaList, signItems);
 
@@ -167,24 +192,100 @@
         }
     }
 
-    // Creates a jar file that contains an empty file.
-    private static void createJar() throws IOException {
-        String testFile = "test";
-        new File(testFile).createNewFile();
-        JarUtils.createJar(TEST_JAR_NAME, testFile);
+    private static SignItem createJarFile(String jar, Manifest m,
+            String... files) throws IOException {
+        JarUtils.createJarFile(Path.of(jar), m, Path.of("."),
+                Arrays.stream(files).map(Path::of).toArray(Path[]::new));
+        return SignItem.build()
+                .signedJar(jar.replaceAll("[.]jar$", ""))
+            .addContentFiles(Arrays.stream(files).collect(Collectors.toList()));
+    }
+
+    private static String createDummyFile(String name) throws IOException {
+        if (name.contains("/")) new File(name).getParentFile().mkdir();
+        try (OutputStream fos = new FileOutputStream(name)) {
+            fos.write(name.getBytes(UTF_8));
+        }
+        return name;
+    }
+
+    // Creates one or more jar files to test
+    private static List<SignItem> createJars() throws IOException {
+        List<SignItem> jarList = new ArrayList<>();
+
+        Manifest m = new Manifest();
+        m.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+
+        // creates a jar file that contains a dummy file
+        jarList.add(createJarFile("test.jar", m, createDummyFile("dummy")));
+
+        if (TEST_COMPREHENSIVE_JAR_CONTENTS) {
+
+            // empty jar file so that jarsigner will add a default manifest
+            jarList.add(createJarFile("empty.jar", m));
+
+            // jar file that contains only an empty manifest with empty main
+            // attributes (due to missing "Manifest-Version" header)
+            JarUtils.createJar("nomainatts.jar");
+            jarList.add(SignItem.build().signedJar("nomainatts"));
+
+            // creates a jar file that contains several files.
+            jarList.add(createJarFile("files.jar", m,
+                    IntStream.range(1, 9).boxed().map(i -> {
+                        try {
+                            return createDummyFile("dummy" + i);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }).toArray(String[]::new)
+            ));
+
+            // forces a line break by exceeding the line width limit of 72 bytes
+            // in the filename and hence manifest entry name
+            jarList.add(createJarFile("longfilename.jar", m,
+                    createDummyFile("test".repeat(20))));
+
+            // another interesting case is with different digest algorithms
+            // resulting in digests broken across line breaks onto continuation
+            // lines. these however are set with the 'digestAlgs' option or
+            // include all digest algorithms by default, see SignTwice.java.
+        }
+
+        return jarList;
+    }
+
+    // updates a signed jar file by adding another file
+    private static List<SignItem> updateJar(SignItem prev) throws IOException {
+        List<SignItem> jarList = new ArrayList<>();
+
+        // sign unmodified jar again
+        Files.copy(Path.of(prev.signedJar + ".jar"),
+                Path.of(prev.signedJar + "-signagainunmodified.jar"));
+        jarList.add(SignItem.build(prev)
+                .signedJar(prev.signedJar + "-signagainunmodified"));
+
+        String oldJar = prev.signedJar;
+        String newJar = oldJar + "-addfile";
+        String triggerUpdateFile = "addfile";
+        JarUtils.updateJar(oldJar + ".jar", newJar + ".jar", triggerUpdateFile);
+        jarList.add(SignItem.build(prev).signedJar(newJar)
+                .addContentFiles(Arrays.asList(triggerUpdateFile)));
+
+        return jarList;
     }
 
     // Creates a key store that includes a set of valid/expired certificates
     // with various algorithms.
     private static List<CertInfo> createCertificates(List<JdkInfo> jdkInfoList)
             throws Throwable {
-        List<CertInfo> certList = new ArrayList<CertInfo>();
-        Set<String> expiredCertFilter = new HashSet<String>();
+        List<CertInfo> certList = new ArrayList<>();
+        Set<String> expiredCertFilter = new HashSet<>();
 
-        for(JdkInfo jdkInfo : jdkInfoList) {
-            for(String keyAlgorithm : KEY_ALGORITHMS) {
-                for(String digestAlgorithm : DIGEST_ALGORITHMS) {
-                    for(int keySize : keySizes(keyAlgorithm)) {
+        for (JdkInfo jdkInfo : jdkInfoList) {
+            for (String keyAlgorithm : keyAlgs()) {
+                if (!jdkInfo.supportsKeyAlg(keyAlgorithm)) continue;
+                for (int keySize : keySizes(keyAlgorithm)) {
+                    for (String digestAlgorithm : digestAlgs()) {
                         for(boolean expired : EXPIRED) {
                             // It creates only one expired certificate for one
                             // key algorithm.
@@ -194,41 +295,60 @@
                             }
 
                             CertInfo certInfo = new CertInfo(
-                                    jdkInfo.version,
+                                    jdkInfo,
                                     keyAlgorithm,
                                     digestAlgorithm,
                                     keySize,
                                     expired);
-                            if (!certList.contains(certInfo)) {
-                                String alias = createCertificate(
-                                        jdkInfo.jdkPath, certInfo);
-                                if (alias != null) {
-                                    certList.add(certInfo);
-                                }
+                            // If the signature algorithm is not supported by the
+                            // JDK, it cannot try to sign jar with this algorithm.
+                            String sigalg = certInfo.sigalg();
+                            if (sigalg != null &&
+                                    !jdkInfo.isSupportedSigalg(sigalg)) {
+                                continue;
                             }
+                            createCertificate(jdkInfo, certInfo);
+                            certList.add(certInfo);
                         }
                     }
                 }
             }
         }
 
+        System.out.println("the keystore contents:");
+        for (JdkInfo jdkInfo : jdkInfoList) {
+            execTool(jdkInfo.jdkPath + "/bin/keytool", new String[] {
+                    "-v",
+                    "-storetype",
+                    "jks",
+                    "-storepass",
+                    PASSWORD,
+                    "-keystore",
+                    KEYSTORE,
+                    "-list"
+            });
+        }
+
         return certList;
     }
 
     // Creates/Updates a key store that adds a certificate with specific algorithm.
-    private static String createCertificate(String jdkPath, CertInfo certInfo)
+    private static void createCertificate(JdkInfo jdkInfo, CertInfo certInfo)
             throws Throwable {
-        String alias = certInfo.alias();
-
-        List<String> arguments = new ArrayList<String>();
+        List<String> arguments = new ArrayList<>();
         arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
         arguments.add("-v");
+        arguments.add("-debug");
         arguments.add("-storetype");
         arguments.add("jks");
-        arguments.add("-genkey");
+        arguments.add("-keystore");
+        arguments.add(KEYSTORE);
+        arguments.add("-storepass");
+        arguments.add(PASSWORD);
+        arguments.add(jdkInfo.majorVersion < 6 ? "-genkey" : "-genkeypair");
         arguments.add("-keyalg");
         arguments.add(certInfo.keyAlgorithm);
-        String sigalg = sigalg(certInfo.digestAlgorithm, certInfo.keyAlgorithm);
+        String sigalg = certInfo.sigalg();
         if (sigalg != null) {
             arguments.add("-sigalg");
             arguments.add(sigalg);
@@ -238,41 +358,29 @@
             arguments.add(certInfo.keySize + "");
         }
         arguments.add("-dname");
-        arguments.add("CN=Test");
+        arguments.add("CN=" + certInfo);
         arguments.add("-alias");
-        arguments.add(alias);
+        arguments.add(certInfo.alias());
         arguments.add("-keypass");
         arguments.add(PASSWORD);
-        arguments.add("-storepass");
-        arguments.add(PASSWORD);
 
         arguments.add("-startdate");
         arguments.add(startDate(certInfo.expired));
         arguments.add("-validity");
+//        arguments.add(DELAY_VERIFY ? "1" : "222"); // > six months no warn
         arguments.add("1");
-        arguments.add("-keystore");
-        arguments.add(KEYSTORE);
 
         OutputAnalyzer outputAnalyzer = execTool(
-                jdkPath + "/bin/keytool",
+                jdkInfo.jdkPath + "/bin/keytool",
                 arguments.toArray(new String[arguments.size()]));
-        if (outputAnalyzer.getExitValue() == 0
-                && !outputAnalyzer.getOutput().matches("[Ee]xception")) {
-            return alias;
-        } else {
-            return null;
+        if (outputAnalyzer.getExitValue() != 0
+                || outputAnalyzer.getOutput().matches("[Ee]xception")
+                || outputAnalyzer.getOutput().matches(Test.ERROR + " ?")) {
+            System.out.println(outputAnalyzer.getOutput());
+            throw new Exception("error generating a key pair: " + arguments);
         }
     }
 
-    private static String sigalg(String digestAlgorithm, String keyAlgorithm) {
-        if (digestAlgorithm == DEFAULT) {
-            return null;
-        }
-
-        String keyName = keyAlgorithm == EC ? "ECDSA" : keyAlgorithm;
-        return digestAlgorithm.replace("-", "") + "with" + keyName;
-    }
-
     // The validity period of a certificate always be 1 day. For creating an
     // expired certificate, the start date is the time before 1 day, then the
     // certificate expires immediately. And for creating a valid certificate,
@@ -280,26 +388,36 @@
     // the certificate will expires in CERT_VALIDITY minutes.
     private static String startDate(boolean expiredCert) {
         CALENDAR.setTime(new Date());
-        CALENDAR.add(Calendar.DAY_OF_MONTH, -1);
-        if (!expiredCert) {
+        if (DELAY_VERIFY || expiredCert) {
+            // corresponds to '-validity 1'
+            CALENDAR.add(Calendar.DAY_OF_MONTH, -1);
+        }
+        if (DELAY_VERIFY && !expiredCert) {
             CALENDAR.add(Calendar.MINUTE, CERT_VALIDITY);
         }
         Date startDate = CALENDAR.getTime();
-        lastCertStartTime = startDate.getTime();
+        if (!expiredCert) {
+            lastCertStartTime = startDate.getTime();
+        }
         return DATE_FORMAT.format(startDate);
     }
 
-    // Retrieves JDK info from the file which is specified by property jdkListFile,
-    // or from property jdkList if jdkListFile is not available.
+    private static String outfile() {
+        return System.getProperty("o");
+    }
+
+    // Retrieves JDK info from the file which is specified by property
+    // jdkListFile, or from property jdkList if jdkListFile is not available.
     private static List<JdkInfo> jdkInfoList() throws Throwable {
         String[] jdkList = list("jdkList");
         if (jdkList.length == 0) {
-            jdkList = new String[] { TEST_JDK };
+            jdkList = new String[] { "TEST_JDK" };
         }
 
-        List<JdkInfo> jdkInfoList = new ArrayList<JdkInfo>();
+        List<JdkInfo> jdkInfoList = new ArrayList<>();
         for (String jdkPath : jdkList) {
-            JdkInfo jdkInfo = new JdkInfo(jdkPath);
+            JdkInfo jdkInfo = "TEST_JDK".equalsIgnoreCase(jdkPath) ?
+                    TEST_JDK_INFO : new JdkInfo(jdkPath);
             // The JDK version must be unique.
             if (!jdkInfoList.contains(jdkInfo)) {
                 jdkInfoList.add(jdkInfo);
@@ -310,12 +428,51 @@
         return jdkInfoList;
     }
 
+    private static List<String> keyAlgs() throws IOException {
+        if (KEY_ALGORITHMS == null) KEY_ALGORITHMS = list("keyAlgs");
+        if (KEY_ALGORITHMS.length == 0)
+            return Arrays.asList(DEFAULT_KEY_ALGORITHMS);
+        return Arrays.stream(KEY_ALGORITHMS).map(a -> a.split(";")[0])
+                .collect(Collectors.toList());
+    }
+
+    // Return key sizes according to the specified key algorithm.
+    private static int[] keySizes(String keyAlgorithm) throws IOException {
+        if (KEY_ALGORITHMS == null) KEY_ALGORITHMS = list("keyAlgs");
+        for (String keyAlg : KEY_ALGORITHMS) {
+            String[] split = (keyAlg + " ").split(";");
+            if (keyAlgorithm.equals(split[0].trim()) && split.length > 1) {
+                int sizes[] = new int[split.length - 1];
+                for (int i = 1; i <= sizes.length; i++)
+                    sizes[i - 1] = split[i].isBlank() ? 0 : // default
+                        Integer.parseInt(split[i].trim());
+                return sizes;
+            }
+        }
+
+        // defaults
+        if (RSA.equals(keyAlgorithm) || DSA.equals(keyAlgorithm)) {
+            return new int[] { 1024, 2048, 0 }; // 0 is no keysize specified
+        } else if (EC.equals(keyAlgorithm)) {
+            return new int[] { 384, 571, 0 }; // 0 is no keysize specified
+        } else {
+            throw new RuntimeException("problem determining key sizes");
+        }
+    }
+
+    private static List<String> digestAlgs() throws IOException {
+        if (DIGEST_ALGORITHMS == null) DIGEST_ALGORITHMS = list("digestAlgs");
+        if (DIGEST_ALGORITHMS.length == 0)
+            return Arrays.asList(DEFAULT_DIGEST_ALGORITHMS);
+        return Arrays.asList(DIGEST_ALGORITHMS);
+    }
+
     // Retrieves TSA info from the file which is specified by property tsaListFile,
     // or from property tsaList if tsaListFile is not available.
     private static List<TsaInfo> tsaInfoList() throws IOException {
         String[] tsaList = list("tsaList");
 
-        List<TsaInfo> tsaInfoList = new ArrayList<TsaInfo>();
+        List<TsaInfo> tsaInfoList = new ArrayList<>();
         for (int i = 0; i < tsaList.length; i++) {
             String[] values = tsaList[i].split(";digests=");
 
@@ -324,25 +481,30 @@
                 digests = values[1].split(",");
             }
 
-            TsaInfo bufTsa = new TsaInfo(i, values[0]);
-
+            String tsaUrl = values[0];
+            if (tsaUrl.isEmpty() || tsaUrl.equalsIgnoreCase("notsa")) {
+                tsaUrl = null;
+            }
+            TsaInfo bufTsa = new TsaInfo(i, tsaUrl);
             for (String digest : digests) {
-                bufTsa.addDigest(digest);
+                bufTsa.addDigest(digest.toUpperCase());
             }
-
             tsaInfoList.add(bufTsa);
         }
 
+        if (tsaInfoList.size() == 0) {
+            throw new RuntimeException("TSA service is mandatory unless "
+                    + "'notsa' specified explicitly.");
+        }
         return tsaInfoList;
     }
 
-    private static String[] list(String listProp)
-            throws IOException {
+    private static String[] list(String listProp) throws IOException {
         String listFileProp = listProp + "File";
         String listFile = System.getProperty(listFileProp);
         if (!isEmpty(listFile)) {
             System.out.println(listFileProp + "=" + listFile);
-            List<String> list = new ArrayList<String>();
+            List<String> list = new ArrayList<>();
             BufferedReader reader = new BufferedReader(
                     new FileReader(listFile));
             String line;
@@ -369,26 +531,47 @@
     // JDKs (verifiers), including the signer itself, try to verify the signed
     // jars respectively.
     private static List<SignItem> test(List<JdkInfo> jdkInfoList,
-            List<TsaInfo> tsaInfoList, List<CertInfo> certList)
-            throws Throwable {
+            List<TsaInfo> tsaInfoList, List<CertInfo> certList,
+            List<SignItem> jars) throws Throwable {
         detailsOutput.transferPhase();
-        List<SignItem> signItems = signing(jdkInfoList, tsaInfoList, certList);
+        List<SignItem> signItems = new ArrayList<>();
+        signItems.addAll(signing(jdkInfoList, tsaInfoList, certList, jars));
+        if (TEST_JAR_UPDATE) {
+            signItems.addAll(signing(jdkInfoList, tsaInfoList, certList,
+                    updating(signItems.stream().filter(
+                            x -> x.status != Status.ERROR)
+                    .collect(Collectors.toList()))));
+        }
 
         detailsOutput.transferPhase();
         for (SignItem signItem : signItems) {
             for (JdkInfo verifierInfo : jdkInfoList) {
-                // JDK 6 doesn't support EC
-                if (!verifierInfo.isJdk6()
-                        || signItem.certInfo.keyAlgorithm != EC) {
-                    verifying(signItem, VerifyItem.build(verifierInfo));
-                }
+                if (!verifierInfo.supportsKeyAlg(
+                        signItem.certInfo.keyAlgorithm)) continue;
+                VerifyItem verifyItem = VerifyItem.build(verifierInfo);
+                verifyItem.addSignerCertInfos(signItem);
+                signItem.addVerifyItem(verifyItem);
+                verifying(signItem, verifyItem);
             }
         }
 
+        // if lastCertExpirationTime passed already now, probably some
+        // certificate was already expired during jar signature verification
+        // (jarsigner -verify) and the test should probably be repeated with an
+        // increased validity period -DcertValidity CERT_VALIDITY
+        long lastCertExpirationTime = lastCertStartTime + 24 * 60 * 60 * 1000;
+        if (lastCertExpirationTime < System.currentTimeMillis()) {
+            throw new AssertionError("CERT_VALIDITY (" + CERT_VALIDITY
+                    + " [minutes]) was too short. "
+                    + "Creating and signing the jars took longer, "
+                    + "presumably at least "
+                    + ((lastCertExpirationTime - System.currentTimeMillis())
+                            / 60 * 1000 + CERT_VALIDITY) + " [minutes].");
+        }
+
         if (DELAY_VERIFY) {
             detailsOutput.transferPhase();
             System.out.print("Waiting for delay verifying");
-            long lastCertExpirationTime = lastCertStartTime + 24 * 60 * 60 * 1000;
             while (System.currentTimeMillis() < lastCertExpirationTime) {
                 TimeUnit.SECONDS.sleep(30);
                 System.out.print(".");
@@ -404,128 +587,98 @@
         }
 
         detailsOutput.transferPhase();
-
         return signItems;
     }
 
     private static List<SignItem> signing(List<JdkInfo> jdkInfos,
-            List<TsaInfo> tsaList, List<CertInfo> certList) throws Throwable {
-        List<SignItem> signItems = new ArrayList<SignItem>();
+            List<TsaInfo> tsaList, List<CertInfo> certList,
+            List<SignItem> unsignedJars) throws Throwable {
+        List<SignItem> signItems = new ArrayList<>();
 
-        Set<String> signFilter = new HashSet<String>();
+        for (CertInfo certInfo : certList) {
+            JdkInfo signerInfo = certInfo.jdkInfo;
+            String keyAlgorithm = certInfo.keyAlgorithm;
+            String sigDigestAlgorithm = certInfo.digestAlgorithm;
+            int keySize = certInfo.keySize;
+            boolean expired = certInfo.expired;
 
-        for (JdkInfo signerInfo : jdkInfos) {
-            for (String keyAlgorithm : KEY_ALGORITHMS) {
-                // JDK 6 doesn't support EC
-                if (signerInfo.isJdk6() && keyAlgorithm == EC) {
-                    continue;
+            for (String jarDigestAlgorithm : digestAlgs()) {
+                if (DEFAULT.equals(jarDigestAlgorithm)) {
+                    jarDigestAlgorithm = null;
                 }
 
-                for (String digestAlgorithm : DIGEST_ALGORITHMS) {
-                    String sigalg = sigalg(digestAlgorithm, keyAlgorithm);
-                    // If the signature algorithm is not supported by the JDK,
-                    // it cannot try to sign jar with this algorithm.
-                    if (sigalg != null && !signerInfo.isSupportedSigalg(sigalg)) {
-                        continue;
+                for (TsaInfo tsaInfo : tsaList) {
+                    String tsaUrl = tsaInfo.tsaUrl;
+
+                    List<String> tsaDigestAlgs = digestAlgs();
+                    // no point in specifying a tsa digest algorithm
+                    // for no TSA, except maybe it would issue a warning.
+                    if (tsaUrl == null) tsaDigestAlgs = Arrays.asList(DEFAULT);
+                    // If the JDK doesn't support option -tsadigestalg, the
+                    // associated cases can just be ignored.
+                    if (!signerInfo.supportsTsadigestalg) {
+                        tsaDigestAlgs = Arrays.asList(DEFAULT);
                     }
+                    for (String tsaDigestAlg : tsaDigestAlgs) {
+                        if (DEFAULT.equals(tsaDigestAlg)) {
+                            tsaDigestAlg = null;
+                        } else if (!tsaInfo.isDigestSupported(tsaDigestAlg)) {
+                            // It has to ignore the digest algorithm, which
+                            // is not supported by the TSA server.
+                            continue;
+                        }
 
-                    // If the JDK doesn't support option -tsadigestalg, the
-                    // associated cases just be ignored.
-                    if (digestAlgorithm != DEFAULT
-                            && !signerInfo.supportsTsadigestalg) {
-                        continue;
-                    }
+                        if (tsaUrl != null && TsaFilter.filter(
+                                signerInfo.version,
+                                tsaDigestAlg,
+                                expired,
+                                tsaInfo.index)) {
+                            continue;
+                        }
 
-                    for (int keySize : keySizes(keyAlgorithm)) {
-                        for (boolean expired : EXPIRED) {
-                            CertInfo certInfo = new CertInfo(
-                                    signerInfo.version,
-                                    keyAlgorithm,
-                                    digestAlgorithm,
-                                    keySize,
-                                    expired);
-                            if (!certList.contains(certInfo)) {
-                                continue;
+                        for (SignItem prevSign : unsignedJars) {
+                            String unsignedJar = prevSign.signedJar;
+
+                            SignItem signItem = SignItem.build(prevSign)
+                                    .certInfo(certInfo)
+                                    .jdkInfo(signerInfo);
+                            String signedJar = unsignedJar + "-" + "JDK_" + (
+                                    signerInfo.version + "-CERT_" + certInfo).
+                                    replaceAll("[^a-z_0-9A-Z.]+", "-");
+
+                            if (jarDigestAlgorithm != null) {
+                                signedJar += "-DIGESTALG_" + jarDigestAlgorithm;
+                                signItem.digestAlgorithm(jarDigestAlgorithm);
                             }
+                            if (tsaUrl == null) {
+                                signItem.tsaIndex(-1);
+                            } else {
+                                signedJar += "-TSA_" + tsaInfo.index;
+                                signItem.tsaIndex(tsaInfo.index);
+                                if (tsaDigestAlg != null) {
+                                    signedJar += "-TSADIGALG_" + tsaDigestAlg;
+                                    signItem.tsaDigestAlgorithm(tsaDigestAlg);
+                                }
+                            }
+                            signItem.signedJar(signedJar);
 
-                            String tsadigestalg = digestAlgorithm != DEFAULT
-                                                ? digestAlgorithm
-                                                : null;
+                            String signingId = signingId(signItem);
+                            detailsOutput.writeAnchorName(signingId,
+                                    "Signing: " + signingId);
 
-                            for (TsaInfo tsaInfo : tsaList) {
-                                // It has to ignore the digest algorithm, which
-                                // is not supported by the TSA server.
-                                if(!tsaInfo.isDigestSupported(tsadigestalg)) {
-                                    continue;
-                                }
-
-                                String tsaUrl = tsaInfo.tsaUrl;
-                                if (TsaFilter.filter(
-                                        signerInfo.version,
-                                        digestAlgorithm,
-                                        expired,
-                                        tsaInfo.index)) {
-                                    tsaUrl = null;
-                                }
-
-                                String signedJar = "JDK_"
-                                        + signerInfo.version + "-CERT_"
-                                        + certInfo
-                                        + (tsaUrl == null
-                                           ? ""
-                                           : "-TSA_" + tsaInfo.index);
-
-                                // It has to ignore the same jar signing.
-                                if (!signFilter.add(signedJar)) {
-                                    continue;
-                                }
-
-                                SignItem signItem = SignItem.build()
-                                        .certInfo(certInfo)
-                                        .version(signerInfo.version)
-                                        .signatureAlgorithm(sigalg)
-                                        .tsaDigestAlgorithm(
-                                                tsaUrl == null
-                                                ? null
-                                                : tsadigestalg)
-                                        .tsaIndex(
-                                                tsaUrl == null
-                                                ? -1
-                                                : tsaInfo.index)
-                                        .signedJar(signedJar);
-                                String signingId = signingId(signItem);
-                                detailsOutput.writeAnchorName(signingId,
-                                        "Signing: " + signingId);
-
-                                OutputAnalyzer signOA = signJar(
-                                        signerInfo.jarsignerPath,
-                                        sigalg,
-                                        tsadigestalg,
-                                        tsaUrl,
-                                        certInfo.alias(),
-                                        signedJar);
-                                Status signingStatus = signingStatus(signOA);
-                                signItem.status(signingStatus);
-
-                                if (signingStatus != Status.ERROR) {
-                                    // Using the testing JDK, which is specified
-                                    // by jtreg option "-jdk", to verify the
-                                    // signed jar and extract the signature
-                                    // algorithm and timestamp digest algorithm.
-                                    String output = verifyJar(TEST_JARSIGNER,
-                                            signedJar).getOutput();
-                                    signItem.extractedSignatureAlgorithm(
-                                            extract(output,
-                                                    " *Signature algorithm.*",
-                                                    ".*: |,.*"));
-                                    signItem.extractedTsaDigestAlgorithm(
-                                            extract(output,
-                                                    " *Timestamp digest algorithm.*",
-                                                    ".*: "));
-                                }
-
-                                signItems.add(signItem);
-                            }
+                            OutputAnalyzer signOA = signJar(
+                                    signerInfo.jarsignerPath,
+                                    certInfo.sigalg(),
+                                    jarDigestAlgorithm,
+                                    tsaDigestAlg,
+                                    tsaUrl,
+                                    certInfo.alias(),
+                                    unsignedJar,
+                                    signedJar);
+                            Status signingStatus = signingStatus(signOA,
+                                    tsaUrl != null);
+                            signItem.status(signingStatus);
+                            signItems.add(signItem);
                         }
                     }
                 }
@@ -535,94 +688,164 @@
         return signItems;
     }
 
+    private static List<SignItem> updating(List<SignItem> prevSignItems)
+            throws IOException {
+        List<SignItem> updateItems = new ArrayList<>();
+        for (SignItem prevSign : prevSignItems) {
+            updateItems.addAll(updateJar(prevSign));
+        }
+        return updateItems;
+    }
+
     private static void verifying(SignItem signItem, VerifyItem verifyItem)
             throws Throwable {
-        boolean delayVerify = verifyItem.status == Status.NONE;
-        String verifyingId = verifyingId(signItem, verifyItem, !delayVerify);
+        // TODO: how will be ensured that the first verification is not after valid period expired which is only one minute?
+        boolean delayVerify = verifyItem.status != Status.NONE;
+        String verifyingId = verifyingId(signItem, verifyItem, delayVerify);
         detailsOutput.writeAnchorName(verifyingId, "Verifying: " + verifyingId);
+        OutputAnalyzer verifyOA = verifyJar(verifyItem.jdkInfo.jarsignerPath,
+                signItem.signedJar, verifyItem.certInfo == null ? null :
+                verifyItem.certInfo.alias());
+        Status verifyingStatus = verifyingStatus(signItem, verifyItem, verifyOA);
 
-        OutputAnalyzer verifyOA = verifyJar(verifyItem.jdkInfo.jarsignerPath,
-                signItem.signedJar);
-        Status verifyingStatus = verifyingStatus(verifyOA);
-
-        // It checks if the default timestamp digest algorithm is SHA-256.
-        if (verifyingStatus != Status.ERROR
-                && signItem.tsaDigestAlgorithm == null) {
-            verifyingStatus = signItem.extractedTsaDigestAlgorithm != null
-                                    && !signItem.extractedTsaDigestAlgorithm.matches("SHA-?256")
-                            ? Status.ERROR
-                            : verifyingStatus;
-            if (verifyingStatus == Status.ERROR) {
-                System.out.println("The default tsa digest is not SHA-256: "
-                    + signItem.extractedTsaDigestAlgorithm);
-            }
+        try {
+            String match = "^  ("
+                    + "  Signature algorithm: " + signItem.certInfo.
+                            expectedSigalg() + ", " + signItem.certInfo.
+                            expectedKeySize() + "-bit key"
+                    + ")|("
+                    + "  Digest algorithm: " + signItem.expectedDigestAlg()
+                    + (signItem.tsaIndex < 0 ? "" :
+                      ")|("
+                    + "Timestamped by \".+\" on .*"
+                    + ")|("
+                    + "  Timestamp digest algorithm: "
+                            + signItem.expectedTsaDigestAlg()
+                    + ")|("
+                    + "  Timestamp signature algorithm: .*"
+                      )
+                    + ")$";
+            verifyOA.stdoutShouldMatchByLine(
+                    "^- Signed by \"CN=" +  signItem.certInfo.toString()
+                            .replaceAll("[.]", "[.]") + "\"$",
+                    "^(- Signed by \"CN=.+\")?$",
+                    match);
+        } catch (Throwable e) {
+            e.printStackTrace();
+            verifyingStatus = Status.ERROR;
         }
 
-        if (delayVerify) {
-            signItem.addVerifyItem(verifyItem.status(verifyingStatus));
+        if (!delayVerify) {
+            verifyItem.status(verifyingStatus);
         } else {
             verifyItem.delayStatus(verifyingStatus);
         }
-    }
 
-    // Return key sizes according to the specified key algorithm.
-    private static int[] keySizes(String keyAlgorithm) {
-        if (keyAlgorithm == RSA || keyAlgorithm == DSA) {
-            return new int[] { 1024, 2048, 0 };
-        } else if (keyAlgorithm == EC) {
-            return new int[] { 384, 571, 0 };
+        if (verifyItem.prevVerify != null) {
+            verifying(signItem, verifyItem.prevVerify);
         }
-
-        return null;
     }
 
     // Determines the status of signing.
-    private static Status signingStatus(OutputAnalyzer outputAnalyzer) {
-        if (outputAnalyzer.getExitValue() == 0) {
-            if (outputAnalyzer.getOutput().contains(Test.WARNING)) {
-                return Status.WARNING;
-            } else {
-                return Status.NORMAL;
-            }
-        } else {
+    private static Status signingStatus(OutputAnalyzer outputAnalyzer,
+            boolean tsa) {
+        if (outputAnalyzer.getExitValue() != 0) {
             return Status.ERROR;
         }
+        if (!outputAnalyzer.getOutput().contains(Test.JAR_SIGNED)) {
+            return Status.ERROR;
+        }
+
+        boolean warning = false;
+        for (String line : outputAnalyzer.getOutput().lines()
+                .toArray(String[]::new)) {
+            if (line.matches(Test.ERROR + " ?")) return Status.ERROR;
+            if (line.matches(Test.WARNING + " ?")) warning = true;
+        }
+        return warning ? Status.WARNING : Status.NORMAL;
     }
 
     // Determines the status of verifying.
-    private static Status verifyingStatus(OutputAnalyzer outputAnalyzer) {
-        if (outputAnalyzer.getExitValue() == 0) {
-            String output = outputAnalyzer.getOutput();
-            if (!output.contains(Test.JAR_VERIFIED)) {
-                return Status.ERROR;
-            } else if (output.contains(Test.WARNING)) {
-                return Status.WARNING;
-            } else {
-                return Status.NORMAL;
+    private static Status verifyingStatus(SignItem signItem, VerifyItem
+            verifyItem, OutputAnalyzer outputAnalyzer) {
+        List<String> expectedSignedContent = new ArrayList<>();
+        if (verifyItem.certInfo == null) {
+            expectedSignedContent.addAll(signItem.jarContents);
+        } else {
+            SignItem i = signItem;
+            while (i != null) {
+                if (i.certInfo != null && i.certInfo.equals(verifyItem.certInfo)) {
+                    expectedSignedContent.addAll(i.jarContents);
+                }
+                i = i.prevSign;
             }
-        } else {
+        }
+        List<String> expectedUnsignedContent =
+                new ArrayList<>(signItem.jarContents);
+        expectedUnsignedContent.removeAll(expectedSignedContent);
+
+        int expectedExitCode = !STRICT || expectedUnsignedContent.isEmpty() ? 0 : 32;
+        if (outputAnalyzer.getExitValue() != expectedExitCode) {
+            System.out.println("verifyingStatus: error: exit code != " + expectedExitCode + ": " + outputAnalyzer.getExitValue() + " != " + expectedExitCode);
             return Status.ERROR;
         }
-    }
+        String expectedSuccessMessage = expectedUnsignedContent.isEmpty() ?
+                Test.JAR_VERIFIED : Test.JAR_VERIFIED_WITH_SIGNER_ERRORS;
+        if (!outputAnalyzer.getOutput().contains(expectedSuccessMessage)) {
+            System.out.println("verifyingStatus: error: expectedSuccessMessage not found: " + expectedSuccessMessage);
+            return Status.ERROR;
+        }
 
-    // Extracts string from text by specified patterns.
-    private static String extract(String text, String linePattern,
-            String replacePattern) {
-        Matcher lineMatcher = Pattern.compile(linePattern).matcher(text);
-        if (lineMatcher.find()) {
-            String line = lineMatcher.group(0);
-            return line.replaceAll(replacePattern, "");
-        } else {
-            return null;
+        boolean tsa = signItem.tsaIndex >= 0;
+        boolean warning = false;
+        for (String line : outputAnalyzer.getOutput().lines()
+                .toArray(String[]::new)) {
+            if (line.isBlank()) continue;
+            if (Test.JAR_VERIFIED.equals(line)) continue;
+            if (line.matches(Test.ERROR + " ?") && expectedExitCode == 0) {
+                System.out.println("verifyingStatus: error: line.matches(" + Test.ERROR + "\" ?\"): " + line);
+                return Status.ERROR;
+            }
+            if (line.matches(Test.WARNING + " ?")) {
+                warning = true;
+                continue;
+            }
+            if (!warning) continue;
+            line = line.strip();
+            if (Test.NOT_YET_VALID_CERT_SIGNING_WARNING.equals(line)) continue;
+            if (Test.HAS_EXPIRING_CERT_SIGNING_WARNING.equals(line)) continue;
+            if (Test.HAS_EXPIRING_CERT_VERIFYING_WARNING.equals(line)) continue;
+            if (line.matches("^" + Test.NO_TIMESTAMP_SIGNING_WARN_TEMPLATE
+                    .replaceAll(
+                        "\\(%1\\$tY-%1\\$tm-%1\\$td\\)", "\\\\([^\\\\)]+\\\\)"
+                        + "( or after any future revocation date)?")
+                    .replaceAll("[.]", "[.]") + "$") && !tsa) continue;
+            if (line.matches("^" + Test.NO_TIMESTAMP_VERIFYING_WARN_TEMPLATE
+                    .replaceAll("\\(as early as %1\\$tY-%1\\$tm-%1\\$td\\)",
+                        "\\\\([^\\\\)]+\\\\)"
+                        + "( or after any future revocation date)?")
+                    .replaceAll("[.]", "[.]") + "$") && !tsa) continue;
+            if (line.matches("^This jar contains signatures that do(es)? not "
+                    + "include a timestamp[.] Without a timestamp, users may "
+                    + "not be able to validate this jar after the signer "
+                    + "certificate's expiration date \\([^\\)]+\\) or after "
+                    + "any future revocation date[.]") && !tsa) continue;
+            if (Test.CERTIFICATE_SELF_SIGNED.equals(line)) continue;
+            if (Test.HAS_EXPIRED_CERT_VERIFYING_WARNING.equals(line)
+                    && signItem.certInfo.expired) continue;
+            System.out.println("verifyingStatus: unexpected line: " + line);
+            return Status.ERROR; // treat unexpected warnings as error
         }
+        return warning ? Status.WARNING : Status.NORMAL;
     }
 
     // Using specified jarsigner to sign the pre-created jar with specified
     // algorithms.
     private static OutputAnalyzer signJar(String jarsignerPath, String sigalg,
-            String tsadigestalg, String tsa, String alias, String signedJar)
-            throws Throwable {
-        List<String> arguments = new ArrayList<String>();
+            String jarDigestAlgorithm,
+            String tsadigestalg, String tsa, String alias, String unsignedJar,
+            String signedJar) throws Throwable {
+        List<String> arguments = new ArrayList<>();
 
         if (PROXY_HOST != null && PROXY_PORT != null) {
             arguments.add("-J-Dhttp.proxyHost=" + PROXY_HOST);
@@ -633,6 +856,10 @@
         arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
         arguments.add("-debug");
         arguments.add("-verbose");
+        if (jarDigestAlgorithm != null) {
+            arguments.add("-digestalg");
+            arguments.add(jarDigestAlgorithm);
+        }
         if (sigalg != null) {
             arguments.add("-sigalg");
             arguments.add(sigalg);
@@ -649,28 +876,34 @@
         arguments.add(KEYSTORE);
         arguments.add("-storepass");
         arguments.add(PASSWORD);
+        arguments.add("-sigfile");
+        arguments.add(nextSigfileName(alias, unsignedJar, signedJar));
         arguments.add("-signedjar");
         arguments.add(signedJar + ".jar");
-        arguments.add(TEST_JAR_NAME);
+        arguments.add(unsignedJar + ".jar");
         arguments.add(alias);
 
-        OutputAnalyzer outputAnalyzer = execTool(
-                jarsignerPath,
+        OutputAnalyzer outputAnalyzer = execTool(jarsignerPath,
                 arguments.toArray(new String[arguments.size()]));
         return outputAnalyzer;
     }
 
     // Using specified jarsigner to verify the signed jar.
     private static OutputAnalyzer verifyJar(String jarsignerPath,
-            String signedJar) throws Throwable {
-        OutputAnalyzer outputAnalyzer = execTool(
-                jarsignerPath,
-                "-J-Djava.security.properties=" + JAVA_SECURITY,
-                "-debug",
-                "-verbose",
-                "-certs",
-                "-keystore", KEYSTORE,
-                "-verify", signedJar + ".jar");
+            String signedJar, String alias) throws Throwable {
+        List<String> arguments = new ArrayList<>();
+        arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
+        arguments.add("-debug");
+        arguments.add("-verbose");
+        arguments.add("-certs");
+        arguments.add("-keystore");
+        arguments.add(KEYSTORE);
+        arguments.add("-verify");
+        if (STRICT) arguments.add("-strict");
+        arguments.add(signedJar + ".jar");
+        if (alias != null) arguments.add(alias);
+        OutputAnalyzer outputAnalyzer = execTool(jarsignerPath,
+                arguments.toArray(new String[arguments.size()]));
         return outputAnalyzer;
     }
 
@@ -686,20 +919,24 @@
         report.append("TSA list:\n");
         for(TsaInfo tsaInfo : tsaList) {
             report.append(
-                    String.format("%d=%s%n", tsaInfo.index, tsaInfo.tsaUrl));
+                    String.format("%d=%s%n", tsaInfo.index,
+                            tsaInfo.tsaUrl == null ? "notsa" : tsaInfo.tsaUrl));
         }
         report.append(HtmlHelper.endPre());
 
         report.append(HtmlHelper.startTable());
         // Generates report headers.
-        List<String> headers = new ArrayList<String>();
-        headers.add("[Certificate]");
+        List<String> headers = new ArrayList<>();
+        headers.add("[Jarfile]");
+        headers.add("[Signing Certificate]");
         headers.add("[Signer JDK]");
         headers.add("[Signature Algorithm]");
-        headers.add("[TSA Digest]");
+        headers.add("[Jar Digest Algorithm]");
+        headers.add("[TSA Digest Algorithm]");
         headers.add("[TSA]");
         headers.add("[Signing Status]");
         headers.add("[Verifier JDK]");
+        headers.add("[Verifying Certificate]");
         headers.add("[Verifying Status]");
         if (DELAY_VERIFY) {
             headers.add("[Delay Verifying Status]");
@@ -710,10 +947,11 @@
 
         StringBuilder failedReport = new StringBuilder(report.toString());
 
-        boolean failed = false;
+        boolean failed = signItems.isEmpty();
 
         // Generates report rows.
         for (SignItem signItem : signItems) {
+            failed = failed || signItem.verifyItems.isEmpty();
             for (VerifyItem verifyItem : signItem.verifyItems) {
                 String reportRow = reportRow(signItem, verifyItem);
                 report.append(reportRow);
@@ -767,13 +1005,21 @@
     // ensures the output is in US English.
     private static OutputAnalyzer execTool(String toolPath, String... args)
             throws Throwable {
-        String[] cmd = new String[args.length + 4];
-        cmd[0] = toolPath;
-        cmd[1] = "-J-Duser.language=en";
-        cmd[2] = "-J-Duser.country=US";
-        cmd[3] = "-J-Djava.security.egd=file:/dev/./urandom";
-        System.arraycopy(args, 0, cmd, 4, args.length);
-        return ProcessTools.executeCommand(cmd);
+        long start = System.currentTimeMillis();
+        try {
+
+            String[] cmd = new String[args.length + 4];
+            cmd[0] = toolPath;
+            cmd[1] = "-J-Duser.language=en";
+            cmd[2] = "-J-Duser.country=US";
+            cmd[3] = "-J-Djava.security.egd=file:/dev/./urandom";
+            System.arraycopy(args, 0, cmd, 4, args.length);
+            return ProcessTools.executeCommand(cmd);
+
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("child process duration [ms]: " + (end - start));
+        }
     }
 
     private static class JdkInfo {
@@ -781,17 +1027,20 @@
         private final String jdkPath;
         private final String jarsignerPath;
         private final String version;
+        private final int majorVersion;
         private final boolean supportsTsadigestalg;
 
-        private Map<String, Boolean> sigalgMap = new HashMap<String, Boolean>();
+        private Map<String, Boolean> sigalgMap = new HashMap<>();
 
         private JdkInfo(String jdkPath) throws Throwable {
             this.jdkPath = jdkPath;
             version = execJdkUtils(jdkPath, JdkUtils.M_JAVA_RUNTIME_VERSION);
-            if (version == null || version.trim().isEmpty()) {
+            if (version == null || version.isBlank()) {
                 throw new RuntimeException(
                         "Cannot determine the JDK version: " + jdkPath);
             }
+            majorVersion = Integer.parseInt((version.matches("^1[.].*") ?
+                 version.substring(2) : version).replaceAll("[^0-9].*$", ""));
             jarsignerPath = jarsignerPath(jdkPath);
             supportsTsadigestalg = execTool(jarsignerPath, "-help")
                     .getOutput().contains("-tsadigestalg");
@@ -799,7 +1048,7 @@
 
         private boolean isSupportedSigalg(String sigalg) throws Throwable {
             if (!sigalgMap.containsKey(sigalg)) {
-                boolean isSupported = "true".equalsIgnoreCase(
+                boolean isSupported = Boolean.parseBoolean(
                         execJdkUtils(
                                 jdkPath,
                                 JdkUtils.M_IS_SUPPORTED_SIGALG,
@@ -810,8 +1059,13 @@
             return sigalgMap.get(sigalg);
         }
 
-        private boolean isJdk6() {
-            return version.startsWith("1.6");
+        private boolean isAtLeastMajorVersion(int minVersion) {
+            return majorVersion >= minVersion;
+        }
+
+        private boolean supportsKeyAlg(String keyAlgorithm) {
+            // JDK 6 doesn't support EC
+            return isAtLeastMajorVersion(6) || !EC.equals(keyAlgorithm);
         }
 
         @Override
@@ -839,13 +1093,18 @@
                 return false;
             return true;
         }
+
+        @Override
+        public String toString() {
+            return "JdkInfo[" + version + ", " + jdkPath + "]";
+        }
     }
 
     private static class TsaInfo {
 
         private final int index;
         private final String tsaUrl;
-        private Set<String> digestList = new HashSet<String>();
+        private Set<String> digestList = new HashSet<>();
 
         private TsaInfo(int index, String tsa) {
             this.index = index;
@@ -853,51 +1112,75 @@
         }
 
         private void addDigest(String digest) {
-            if (!ignore(digest)) {
-                digestList.add(digest);
-            }
-        }
-
-        private static boolean ignore(String digest) {
-            return !SHA1.equalsIgnoreCase(digest)
-                    && !SHA256.equalsIgnoreCase(digest)
-                    && !SHA512.equalsIgnoreCase(digest);
+            digestList.add(digest);
         }
 
         private boolean isDigestSupported(String digest) {
             return digest == null || digestList.isEmpty()
                     || digestList.contains(digest);
         }
+
+        @Override
+        public String toString() {
+            return "TsaInfo[" + index + ", " + tsaUrl + "]";
+        }
     }
 
     private static class CertInfo {
 
-        private final String jdkVersion;
+        private static int certCounter;
+
+        // nr distinguishes cert CNs in jarsigner -verify output
+        private final int nr = ++certCounter;
+        private final JdkInfo jdkInfo;
         private final String keyAlgorithm;
         private final String digestAlgorithm;
         private final int keySize;
         private final boolean expired;
 
-        private CertInfo(String jdkVersion, String keyAlgorithm,
+        private CertInfo(JdkInfo jdkInfo, String keyAlgorithm,
                 String digestAlgorithm, int keySize, boolean expired) {
-            this.jdkVersion = jdkVersion;
+            this.jdkInfo = jdkInfo;
             this.keyAlgorithm = keyAlgorithm;
             this.digestAlgorithm = digestAlgorithm;
             this.keySize = keySize;
             this.expired = expired;
         }
 
+        private String sigalg() {
+            return DEFAULT.equals(digestAlgorithm) ? null : expectedSigalg();
+        }
+
+        private String expectedSigalg() {
+            return (DEFAULT.equals(this.digestAlgorithm) ? this.digestAlgorithm
+                    : "SHA-256").replace("-", "") + "with" +
+                    keyAlgorithm + (EC.equals(keyAlgorithm) ? "DSA" : "");
+        }
+
+        private int expectedKeySize() {
+            if (keySize != 0) return keySize;
+
+            // defaults
+            if (RSA.equals(keyAlgorithm) || DSA.equals(keyAlgorithm)) {
+                return 2048;
+            } else if (EC.equals(keyAlgorithm)) {
+                return 256;
+            } else {
+                throw new RuntimeException("problem determining key size");
+            }
+        }
+
         @Override
         public int hashCode() {
             final int prime = 31;
             int result = 1;
             result = prime * result
-                    + ((digestAlgorithm == null) ? 0 : digestAlgorithm.hashCode());
+                    + (digestAlgorithm == null ? 0 : digestAlgorithm.hashCode());
             result = prime * result + (expired ? 1231 : 1237);
             result = prime * result
-                    + ((jdkVersion == null) ? 0 : jdkVersion.hashCode());
+                    + (jdkInfo == null ? 0 : jdkInfo.hashCode());
             result = prime * result
-                    + ((keyAlgorithm == null) ? 0 : keyAlgorithm.hashCode());
+                    + (keyAlgorithm == null ? 0 : keyAlgorithm.hashCode());
             result = prime * result + keySize;
             return result;
         }
@@ -918,10 +1201,10 @@
                 return false;
             if (expired != other.expired)
                 return false;
-            if (jdkVersion == null) {
-                if (other.jdkVersion != null)
+            if (jdkInfo == null) {
+                if (other.jdkInfo != null)
                     return false;
-            } else if (!jdkVersion.equals(other.jdkVersion))
+            } else if (!jdkInfo.equals(other.jdkInfo))
                 return false;
             if (keyAlgorithm == null) {
                 if (other.keyAlgorithm != null)
@@ -934,12 +1217,16 @@
         }
 
         private String alias() {
-            return jdkVersion + "_" + toString();
+            return (jdkInfo.version + "_" + toString())
+                    // lower case for jks due to
+                    // sun.security.provider.JavaKeyStore.JDK.convertAlias
+                    .toLowerCase(Locale.ENGLISH);
         }
 
         @Override
         public String toString() {
-            return keyAlgorithm + "_" + digestAlgorithm
+            return "nr" + nr + "_"
+                    + keyAlgorithm + "_" + digestAlgorithm
                     + (keySize == 0 ? "" : "_" + keySize)
                     + (expired ? "_Expired" : "");
         }
@@ -949,7 +1236,7 @@
     // TSA service with an arbitrary valid/expired certificate.
     private static class TsaFilter {
 
-        private static final Set<Condition> SET = new HashSet<Condition>();
+        private static final Set<Condition> SET = new HashSet<>();
 
         private static boolean filter(String signerVersion,
                 String digestAlgorithm, boolean expiredCert, int tsaIndex) {
@@ -1029,22 +1316,32 @@
 
     private static class SignItem {
 
+        private SignItem prevSign;
         private CertInfo certInfo;
-        private String version;
-        private String signatureAlgorithm;
-        // Signature algorithm that is extracted from verification output.
-        private String extractedSignatureAlgorithm;
+        private JdkInfo jdkInfo;
+        private String digestAlgorithm;
         private String tsaDigestAlgorithm;
-        // TSA digest algorithm that is extracted from verification output.
-        private String extractedTsaDigestAlgorithm;
         private int tsaIndex;
         private Status status;
+        private String unsignedJar;
         private String signedJar;
+        private List<String> jarContents = new ArrayList<>();
 
-        private List<VerifyItem> verifyItems = new ArrayList<VerifyItem>();
+        private List<VerifyItem> verifyItems = new ArrayList<>();
 
         private static SignItem build() {
-            return new SignItem();
+            return new SignItem()
+                    .addContentFiles(Arrays.asList("META-INF/MANIFEST.MF"));
+        }
+
+        private static SignItem build(SignItem prevSign) {
+            return build().prevSign(prevSign).unsignedJar(prevSign.signedJar)
+                    .addContentFiles(prevSign.jarContents);
+        }
+
+        private SignItem prevSign(SignItem prevSign) {
+            this.prevSign = prevSign;
+            return this;
         }
 
         private SignItem certInfo(CertInfo certInfo) {
@@ -1052,20 +1349,18 @@
             return this;
         }
 
-        private SignItem version(String version) {
-            this.version = version;
+        private SignItem jdkInfo(JdkInfo jdkInfo) {
+            this.jdkInfo = jdkInfo;
             return this;
         }
 
-        private SignItem signatureAlgorithm(String signatureAlgorithm) {
-            this.signatureAlgorithm = signatureAlgorithm;
+        private SignItem digestAlgorithm(String digestAlgorithm) {
+            this.digestAlgorithm = digestAlgorithm;
             return this;
         }
 
-        private SignItem extractedSignatureAlgorithm(
-                String extractedSignatureAlgorithm) {
-            this.extractedSignatureAlgorithm = extractedSignatureAlgorithm;
-            return this;
+        String expectedDigestAlg() {
+            return digestAlgorithm != null ? digestAlgorithm : "SHA-256";
         }
 
         private SignItem tsaDigestAlgorithm(String tsaDigestAlgorithm) {
@@ -1073,10 +1368,8 @@
             return this;
         }
 
-        private SignItem extractedTsaDigestAlgorithm(
-                String extractedTsaDigestAlgorithm) {
-            this.extractedTsaDigestAlgorithm = extractedTsaDigestAlgorithm;
-            return this;
+        String expectedTsaDigestAlg() {
+            return tsaDigestAlgorithm != null ? tsaDigestAlgorithm : "SHA-256";
         }
 
         private SignItem tsaIndex(int tsaIndex) {
@@ -1089,18 +1382,49 @@
             return this;
         }
 
+        private SignItem unsignedJar(String unsignedJar) {
+            this.unsignedJar = unsignedJar;
+            return this;
+        }
+
         private SignItem signedJar(String signedJar) {
             this.signedJar = signedJar;
             return this;
         }
 
+        private SignItem addContentFiles(List<String> files) {
+            this.jarContents.addAll(files);
+            return this;
+        }
+
         private void addVerifyItem(VerifyItem verifyItem) {
             verifyItems.add(verifyItem);
         }
+
+        private boolean isErrorInclPrev() {
+            if (prevSign != null && prevSign.isErrorInclPrev()) {
+                System.out.println("SignItem.isErrorInclPrev: returning true from previous");
+                return true;
+            }
+
+            return status == Status.ERROR;
+        }
+        private List<String> toStringWithPrev(Function<SignItem,String> toStr) {
+            List<String> s = new ArrayList<>();
+            if (prevSign != null) {
+                s.addAll(prevSign.toStringWithPrev(toStr));
+            }
+            if (status != null) { // no status means jar creation or update item
+                s.add(toStr.apply(this));
+            }
+            return s;
+        }
     }
 
     private static class VerifyItem {
 
+        private VerifyItem prevVerify;
+        private CertInfo certInfo;
         private JdkInfo jdkInfo;
         private Status status = Status.NONE;
         private Status delayStatus = Status.NONE;
@@ -1111,15 +1435,54 @@
             return verifyItem;
         }
 
+        private VerifyItem certInfo(CertInfo certInfo) {
+            this.certInfo = certInfo;
+            return this;
+        }
+
+        private void addSignerCertInfos(SignItem signItem) {
+            VerifyItem prevVerify = this;
+            CertInfo lastCertInfo = null;
+            while (signItem != null) {
+                // (signItem.certInfo == null) means create or update jar step
+                if (signItem.certInfo != null
+                        && !signItem.certInfo.equals(lastCertInfo)) {
+                    lastCertInfo = signItem.certInfo;
+                    prevVerify = prevVerify.prevVerify =
+                            build(jdkInfo).certInfo(signItem.certInfo);
+                }
+                signItem = signItem.prevSign;
+            }
+        }
+
         private VerifyItem status(Status status) {
             this.status = status;
             return this;
         }
 
+        private boolean isErrorInclPrev() {
+            if (prevVerify != null && prevVerify.isErrorInclPrev()) {
+                System.out.println("VerifyItem.isErrorInclPrev: returning true from previous");
+                return true;
+            }
+
+            return status == Status.ERROR || delayStatus == Status.ERROR;
+        }
+
         private VerifyItem delayStatus(Status status) {
             this.delayStatus = status;
             return this;
         }
+
+        private List<String> toStringWithPrev(
+                Function<VerifyItem,String> toStr) {
+            List<String> s = new ArrayList<>();
+            if (prevVerify != null) {
+                s.addAll(prevVerify.toStringWithPrev(toStr));
+            }
+            s.add(toStr.apply(this));
+            return s;
+        }
     }
 
     // The identifier for a specific signing.
@@ -1130,32 +1493,41 @@
     // The identifier for a specific verifying.
     private static String verifyingId(SignItem signItem, VerifyItem verifyItem,
             boolean delayVerify) {
-        return "S_" + signingId(signItem) + "-" + (delayVerify ? "DV" : "V")
-                + "_" + verifyItem.jdkInfo.version;
+        return signingId(signItem) + (delayVerify ? "-DV" : "-V")
+                + "_" + verifyItem.jdkInfo.version +
+                (verifyItem.certInfo == null ? "" : "_" + verifyItem.certInfo);
     }
 
     private static String reportRow(SignItem signItem, VerifyItem verifyItem) {
-        List<String> values = new ArrayList<String>();
-        values.add(signItem.certInfo.toString());
-        values.add(signItem.version);
-        values.add(null2Default(signItem.signatureAlgorithm,
-                signItem.extractedSignatureAlgorithm));
-        values.add(signItem.tsaIndex == -1
-                   ? ""
-                   : null2Default(signItem.tsaDigestAlgorithm,
-                        signItem.extractedTsaDigestAlgorithm));
-        values.add(signItem.tsaIndex == -1 ? "" : signItem.tsaIndex + "");
-        values.add(HtmlHelper.anchorLink(
+        List<String> values = new ArrayList<>();
+        Consumer<Function<SignItem, String>> s_values_add = f -> {
+            values.add(String.join("<br/><br/>", signItem.toStringWithPrev(f)));
+        };
+        Consumer<Function<VerifyItem, String>> v_values_add = f -> {
+            values.add(String.join("<br/><br/>", verifyItem.toStringWithPrev(f)));
+        };
+        s_values_add.accept(i -> i.unsignedJar + " -> " + i.signedJar);
+        s_values_add.accept(i -> i.certInfo.toString());
+        s_values_add.accept(i -> i.jdkInfo.version);
+        s_values_add.accept(i -> i.certInfo.expectedSigalg());
+        s_values_add.accept(i ->
+                null2Default(i.digestAlgorithm, i.expectedDigestAlg()));
+        s_values_add.accept(i -> i.tsaIndex == -1 ? "" :
+                null2Default(i.tsaDigestAlgorithm, i.expectedTsaDigestAlg()));
+        s_values_add.accept(i -> i.tsaIndex == -1 ? "" : i.tsaIndex + "");
+        s_values_add.accept(i -> HtmlHelper.anchorLink(
                 PhaseOutputStream.fileName(PhaseOutputStream.Phase.SIGNING),
-                signingId(signItem),
-                signItem.status.toString()));
+                signingId(i),
+                "" + i.status));
         values.add(verifyItem.jdkInfo.version);
-        values.add(HtmlHelper.anchorLink(
+        v_values_add.accept(i ->
+                i.certInfo == null ? "no alias" : "" + i.certInfo);
+        v_values_add.accept(i -> HtmlHelper.anchorLink(
                 PhaseOutputStream.fileName(PhaseOutputStream.Phase.VERIFYING),
-                verifyingId(signItem, verifyItem, false),
-                verifyItem.status.toString()));
+                verifyingId(signItem, i, false),
+                "" + i.status.toString()));
         if (DELAY_VERIFY) {
-            values.add(HtmlHelper.anchorLink(
+            v_values_add.accept(i -> HtmlHelper.anchorLink(
                     PhaseOutputStream.fileName(
                             PhaseOutputStream.Phase.DELAY_VERIFYING),
                     verifyingId(signItem, verifyItem, true),
@@ -1165,19 +1537,54 @@
         return HtmlHelper.htmlRow(values.toArray(new String[values.size()]));
     }
 
-    private static boolean isFailed(SignItem signItem,
-            VerifyItem verifyItem) {
-        return signItem.status == Status.ERROR
-                || verifyItem.status == Status.ERROR
-                || verifyItem.delayStatus == Status.ERROR;
+    private static boolean isFailed(SignItem signItem, VerifyItem verifyItem) {
+        System.out.println("isFailed: signItem = " + signItem + ", verifyItem = " + verifyItem);
+        // TODO: except known failing cases
+
+        // Note about isAtLeastMajorVersion in the following conditions:
+        // signItem.jdkInfo is the jdk which signed the jar last and
+        // signItem.prevSign.jdkInfo is the jdk which signed the jar first
+        // assuming only two successive signatures as there actually are now.
+        // the first signature always works and always has. subject here is
+        // the update of an already signed jar. the following conditions always
+        // depend on the second jdk that updated the jar with another signature
+        // and the first one (signItem(.prevSign)+.jdkInfo) can be ignored.
+        // this is different for verifyItem. verifyItem.prevVerify refers to
+        // the first signature created by signItem(.prevSign)+.jdkInfo.
+        // all verifyItem(.prevVerify)+.jdkInfo however point always to the same
+        // jdk, only their certInfo is different. the same signatures are
+        // verified with different jdks in different top-level VerifyItems
+        // attached directly to signItem.verifyItems and not to
+        // verifyItem.prevVerify.
+
+        // ManifestDigester fails to parse manifests ending in '\r' with
+        // IndexOutOfBoundsException at ManifestDigester.java:87 before 8217375
+        if (signItem.signedJar.startsWith("eofr")
+                && !signItem.jdkInfo.isAtLeastMajorVersion(13)
+                && !verifyItem.jdkInfo.isAtLeastMajorVersion(13)) return false;
+
+        // if there is no blank line after main attributes, JarSigner adds
+        // individual sections nevertheless without being properly delimited
+        // in JarSigner.java:777..790 without checking for blank line
+        // before 8217375
+//        if (signItem.signedJar.startsWith("eofn-")
+//                && signItem.signedJar.contains("-addfile-")
+//                && !signItem.jdkInfo.isAtLeastMajorVersion(13)
+//                && !verifyItem.jdkInfo.isAtLeastMajorVersion(13)) return false; // FIXME
+
+//        System.out.println("isFailed: signItem.isErrorInclPrev() " + signItem.isErrorInclPrev());
+//        System.out.println("isFailed: verifyItem.isErrorInclPrev() " + verifyItem.isErrorInclPrev());
+        boolean isFailed = signItem.isErrorInclPrev() || verifyItem.isErrorInclPrev();
+        System.out.println("isFailed: returning " + isFailed);
+        return isFailed;
     }
 
     // If a value is null, then displays the default value or N/A.
     private static String null2Default(String value, String defaultValue) {
-        return value == null
-               ? DEFAULT + "(" + (defaultValue == null
+        return value != null ? value :
+               DEFAULT + "(" + (defaultValue == null
                                   ? "N/A"
-                                  : defaultValue) + ")"
-               : value;
+                                  : defaultValue) + ")";
     }
+
 }
--- a/test/jdk/sun/security/tools/jarsigner/compatibility/DetailsOutputStream.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/compatibility/DetailsOutputStream.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -34,8 +34,9 @@
 
     private PhaseOutputStream phaseOutputStream = new PhaseOutputStream();
 
-    public DetailsOutputStream() throws FileNotFoundException {
-        super("details.out", true);
+    public DetailsOutputStream(String filename) throws FileNotFoundException {
+        super(filename != null && !filename.isEmpty() ? filename :
+            "details.out", true);
     }
 
     public void transferPhase() throws IOException {
--- a/test/jdk/sun/security/tools/jarsigner/compatibility/HtmlHelper.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/compatibility/HtmlHelper.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -60,7 +60,7 @@
     }
 
     public static String startTable() {
-        return startTag("table " + STYLE);
+        return startTag("table border=\"1\" padding=\"1\" cellspacing=\"0\" " + STYLE);
     }
 
     public static String endTable() {
@@ -68,19 +68,19 @@
     }
 
     public static String startTr() {
-        return startTag("tr");
+        return "\t" + startTag("tr") + "\n";
     }
 
     public static String endTr() {
-        return endTag("tr");
+        return "\t" + endTag("tr") + "\n";
     }
 
     public static String startTd() {
-        return startTag("td");
+        return "\t\t" + startTag("td");
     }
 
     public static String endTd() {
-        return endTag("td");
+        return endTag("td") + "\n";
     }
 
     public static String startTag(String tag) {
@@ -92,7 +92,7 @@
     }
 
     public static String anchorName(String name, String text) {
-        return "<a name=" + name + ">" + text + "</a>";
+        return "<a name=" + name + "><hr/>" + text + "</a>";
     }
 
     public static String anchorLink(String file, String anchorName,
--- a/test/jdk/sun/security/tools/jarsigner/compatibility/JdkUtils.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/compatibility/JdkUtils.java	Thu Jul 18 08:53:06 2019 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -21,41 +21,59 @@
  * questions.
  */
 
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.Signature;
+import static java.util.Arrays.asList;
 
 /*
  * This class is used for returning some specific JDK information.
  */
 public class JdkUtils {
 
+    private enum Alg {
+        KEY, SIG, DIGEST;
+    }
+
     static final String M_JAVA_RUNTIME_VERSION = "javaRuntimeVersion";
+    static final String M_IS_SUPPORTED_KEYALG = "isSupportedKeyalg";
     static final String M_IS_SUPPORTED_SIGALG = "isSupportedSigalg";
+    static final String M_IS_SUPPORTED_DIGESTALG = "isSupportedDigestalg";
 
     // Returns the JDK build version.
     static String javaRuntimeVersion() {
         return System.getProperty("java.runtime.version");
     }
 
-    // Checks if the specified signature algorithm is supported by the JDK.
-    static boolean isSupportedSigalg(String sigalg) {
-        boolean isSupported = false;
+    // Checks if the specified algorithm is supported by the JDK.
+    static boolean isSupportedAlg(Alg algType, String algName) {
         try {
-            isSupported = Signature.getInstance(sigalg) != null;
+            switch (algType) {
+            case KEY:
+                return KeyPairGenerator.getInstance(algName) != null;
+            case SIG:
+                return Signature.getInstance(algName) != null;
+            case DIGEST:
+                return MessageDigest.getInstance(algName) != null;
+            }
         } catch (NoSuchAlgorithmException e) { }
-
-        if (!isSupported) {
-            System.out.println(sigalg + " is not supported yet.");
-        }
-
-        return isSupported;
+        System.out.println(algName + " is not supported yet.");
+        return false;
     }
 
     public static void main(String[] args) {
         if (M_JAVA_RUNTIME_VERSION.equals(args[0])) {
             System.out.print(javaRuntimeVersion());
+        } else if (M_IS_SUPPORTED_KEYALG.equals(args[0])) {
+            System.out.print(isSupportedAlg(Alg.KEY, args[1]));
         } else if (M_IS_SUPPORTED_SIGALG.equals(args[0])) {
-            System.out.print(isSupportedSigalg(args[1]));
+            System.out.print(isSupportedAlg(Alg.SIG, args[1]));
+        } else if (M_IS_SUPPORTED_DIGESTALG.equals(args[0])) {
+            System.out.print(isSupportedAlg(Alg.DIGEST, args[1]));
+        } else {
+            throw new IllegalArgumentException("invalid: " + asList(args));
         }
     }
+
 }
--- a/test/jdk/sun/security/tools/jarsigner/compatibility/README	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/compatibility/README	Thu Jul 18 08:53:06 2019 +0800
@@ -1,4 +1,4 @@
-# Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 #
 # This code is free software; you can redistribute it and/or modify it
@@ -35,6 +35,10 @@
 stdout and stderr to file JTwork/scratch/details.out.
 
 ##### Report Columns #####
+
+Jarfile
+    The filenames used in the tests
+
 Certificate
     Certificate identifier. The identifier consists of specific attributes of
     the certificate. Generally, the naming convention is:
@@ -44,10 +48,23 @@
     The JDK version that signs jar.
 
 Signature Algorithm
-    The signature algorithm used by signing.
+    The signature algorithm used to sign the key as in 'keytool -sigalg'.
 
-TSA Digest
-    The timestamp digest algorithm used by signing.
+    Note: The values displayed in this column are specified to jarsigner only
+    in case a test does not work with a default value.
+    In any case the specified value or expected default value is compared in
+    verifying phase against jarsigner's output and the test fails if it does
+    not match.
+
+Jar Digest Algorithm
+    The digest algorithm used to digest the files contained in the JAR file and
+    the manifest and signature files as in 'jarsigner -digestalg'.
+    See also note above about default values for Signature Algorithm.
+
+TSA Digest Algorithm
+    The timestamp digest algorithm used by TSA as in 'jarsigner -tsadigestalg'.
+    Shows no value if no TSA used.
+    See also note above about default values for Signature Algorithm.
 
 TSA
     TSA URL index. All of TSA URLs and their indices can be found at the top
@@ -80,24 +97,38 @@
     -jdk:<path/to/testing/JDK>
     [-DproxyHost=<host> \
      -DproxyPort=<port> \
+     -Dteeout=filename \
      -DtsaListFile=</url/to/tsaListFile> \
-     -DtsaList=</path/to/tsa1#/path/to/tsa2#/path/to/tsa3#...> \
+     -DtsaList=</path/to/tsa1#/path/to/tsa2#/path/to/tsa3#...|notsa> \
      -DjdkListFile=</path/to/jdkListFile> \
      -DjdkList=</path/to/jdk1#/path/to/jdk2#/path/to/jdk3#...> \
      -DjavaSecurityFile=</path/to/java/security/properties/file> \
      -DdelayVerify=<true|false> \
      -DcertValidity=<[1, 1440]>] \
-    <JDK_REPO>/jdk/test/sun/security/tools/jarsigner/compatibility/Compatibility.java
+    <JDK_REPO>/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java
 
 Besides the common jtreg options, like -jdk, this test introduces a set of
 properties for receiving users' inputs and making the test more flexible. These
-properties are:
+properties are (all to specify after -D as system properties):
+
 proxyHost=<host>
     This property indicates proxy host.
 
 proxyPort=<port>
     This property indicates proxy port. The default value is 80.
 
+o=filename
+    Redirects a copy of what is written to stdout into the specified file which
+    allows for observing progress or problems during a longer running test.
+    (Compatibility test replaces System.out to collect the output but even if
+    it didn't jtreg would not print anything until the test would have ended.)
+    Note that relative paths resolve relatively to some temporary working
+    directory created by jtreg. Defaults to JTwork/scratch/details.out.
+    The specified file is not deleted or emptied and the output is appended
+    though jtreg deletes the default file with the whole directory if this
+    option is not specified or the file point into JTwork/scratch.
+    Example (Bash style): tail -F log & jtreg ... -Do=$(pwd)/log ...
+
 tsaListFile=</path/to/tsaListFile>
     This property indicates a local file, which contains a set of TSA URLs and
     the supported digest algorithms (by optional parameter digests). The format
@@ -112,11 +143,12 @@
     on SHA-1, SHA-256 and SHA-512. So, if other digest algorithms, like SHA-224
     and SHA-384, are listed, they just be ignored.
 
-tsaList=</path/to/tsa1#/path/to/tsa2;digests=SHA-1,SHA-256#...>
+tsaList=</path/to/tsa1#/path/to/tsa2;digests=SHA-1,SHA-256#...|notsa>
     This property directly lists a set of TSAs in command. "#" is the delimiter.
     Note that, if both of tsaListFile and tsaList are specified, only property
-    jdkListFile is selected. If neither of tsaListFile and tsaList is specified,
-    the test will fails immediately.
+    tsaListFile is selected. If neither of tsaListFile and tsaList is specified,
+    the test fails immediately.
+    If tsaList has a value of "notsa", no tsa is used.
 
 jdkListFile=</path/to/jdkListFile>
     This property indicates a local file, which contains a set of local JDK
@@ -129,11 +161,42 @@
 jdkList=</path/to/jdk1#/path/to/jdk2#/path/to/jdk3#...>
     This property directly lists a set of local JDK paths in command. "#" is
     the delimiter.
+    An element "TEST_JDK" as in
+jdkList=</path/to/jdk1#/path/to/jdk2#TEST_JDK#...>
+    adds the testing JDK, which is specified by jtreg option -jdk, to the jdk
+    list. All signed jars are verified with the current testing JDK, which is
+    specified by jtreg option -jdk, by default in addition to the JDKs given
+    in jdkList but it is not used to also sign jars by default.
+    If neither jdkList nor jdkListFile are specified, the current testing JDK,
+    which is specified by jtreg option -jdk, is used to sign the jars, like:
+jdkList=TEST_JDK
     Note that, if both of jdkListFile and jdkList are specified, only property
     jdkListFile is selected. If neither of jdkListFile nor jdkList is specified,
-    the testing JDK, which is specified by jtreg option -jdk will be used as
+    the testing JDK, which is specified by jtreg option -jdk, will be used as
     the only one JDK in the JDK list.
 
+    The testing JDK, which is specified by jtreg option "-jdk", should include
+    the fix for JDK-8163304. Otherwise, the signature algorithm and timestamp
+    digest algorithm cannot be extracted from verification output. And this JDK
+    should support as many as possible signature algorithms. Anyway the latest
+    JDK build is always recommended.
+
+testComprehensiveJarContents=<false|true>
+    If false, all tests are executed with only one typical JAR file. Otherwise,
+    if true, a whole bunch of JAR files with several edge case contents are
+    fed through the tests such as empty manifest or manifests with non-default
+    line breaks. Default is false.
+
+testJarUpdate=<false|true>
+    If false, all tested JAR files are signed with one JDK and verified with
+    each JDK, same or other. If true, in addition, all JAR files are modified
+    after having been signed, and are then each signed again with each JDK and
+    verified each JDK, same or other. Default is false.
+
+strict=<false|true>
+    If true, '-strict' option is specified to jarsigner along with '-verify'.
+    Default is false.
+
 javaSecurityFile=</path/to/java/security/properties/file>
     This property indicates an alternative java security properties file. The
     default file is the path of file java.scurity that is distributed with
@@ -143,16 +206,36 @@
     This property indicates if doing an additional verifying after all of valid
     certificates expire. The default value is false.
 
+expired=<false|true>
+    This property indicates whether or not all tests should be repeated with an
+    expired certificate. Refers to the certificate validity period and not to
+    TSA. The default value is true.
+
 certValidity=<[1, 1440]>
     This property indicates the remaining validity period in minutes for valid
     certificates. The value range is [1, 1440]. The default value is 1440.
     Note that, if delayVerify is false, this property doesn't take effect.
 
-The testing JDK, which is specified by jtreg option "-jdk", should include the
-fix for JDK-8163304. Otherwise, the signature algorithm and timestamp digest
-algorithm cannot be extracted from verification output. And this JDK should
-support as many as possible signature algorithms. Anyway the latest JDK build
-is always recommended.
+keyAlgs=RSA;1024;2048;#DSA;1024;2048;#EC;384;521;
+    Specifies key algorithms to use in the test. For each key algorithm the
+    sizes it should be tested with can be specified after semicolons and
+    otherwise default values are used. An empty keysize denotes the default
+    keysize and invokes keytool without a keysize specified. On JDK 6 and
+    earlier, EC is not supported and always skipped.
+
+digestAlgs=SHA-1#SHA-256#SHA-384#SHA-512#
+    Specifies the digest algorithms used for both digesting files contained in
+    the JAR file, manifests and signature files as well as certificates (keys)
+    and for TSA.
+    Ignored with TSA for jarsigner versions that don't support '-tsadigestalg'
+    parameter, for digest algorithms specified not to be supported by a TSA
+    server ('digests' sub-option is given to a tsaList item where digest
+    algorithm is not contained in list), or in cases no TSA is used at all
+    ('tsaList=notsa').
+    Note that the same set of digest algorithms is used in all three places
+    (signing the key, digesting the JAR, and for the TSA) and cannot be
+    specified individually except that some TSAs may exclude some digest
+    algorithms.
 
 ##### Examples #####
 $ cat /path/to/jdkList
@@ -177,13 +260,13 @@
 http://zeitstempel.dfn.de
 https://tsp.iaik.tugraz.at/tsp/TspRequest
 
-$ jtreg -va -nr -timeout:100 \
+$ jtreg -va -nr \
     -jdk:/path/to/latest/jdk \
     -DproxyHost=<proxy> -DproxyPort=<port> \
     -DjdkListFile=/path/to/jdkList \
     -DtsaListFile=/path/to/tsaList \
     -DdelayVerify=true -DcertValidity=60 \
-    <JDK_REPO>/jdk/test/sun/security/tools/jarsigner/compatibility/Compatibility.java
+    <JDK_REPO>/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java
 
 The above is a comprehensive usage example. File "jdkList" lists the paths of
 testing JDK builds, and file "tsaList" lists the URLs of TSA services. Some TSAs,
@@ -197,19 +280,28 @@
 If don't want to provide such JDK list and TSA list files, the test allows to
 specify JDKs and TSAs (via properties jdkList and tsaList respectively) in the
 command directly, like the below style,
-$ jtreg -va -nr -timeout:100 \
+$ jtreg -va -nr \
     -jdk:/path/to/latest/jdk \
     -DproxyHost=<proxy> -DproxyPort=<port> \
     -DjdkList=/path/to/jdk6u171-b05#/path/to/jdk7u161-b05#/path/to/jdk8u144-b01#/path/to/jdk9-179 \
     -DtsaList=http://timestamp.comodoca.com/rfc3161#http://timestamp.entrust.net/TSS/RFC3161sha1TS;digests=SHA-1,SHA-256 \
     -DdelayVerify=true -DcertValidity=60 \
-    <JDK_REPO>/jdk/test/sun/security/tools/jarsigner/compatibility/Compatibility.java
+    <JDK_REPO>/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java
 
 Furthermore, here introduces one of the simplest usages. It doesn't specify any
 JDK list, so the testing JDK, which is specified by jtreg option "-jdk", will
 be tested. And it doesn't apply delay verifying, and no proxy is used, and use
 only one TSA. Now, the command is pretty simple and looks like the followings,
-$ jtreg -va -nr -timeout:100 \
+$ jtreg -va -nr \
     -jdk:/path/to/latest/jdk \
     -DtsaList=http://timestamp.comodoca.com/rfc3161 \
-    <JDK_REPO>/jdk/test/sun/security/tools/jarsigner/compatibility/Compatibility.java
\ No newline at end of file
+    <JDK_REPO>/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java
+
+It also works without a tsaList but not without the tsaList argument present
+in order to prevent it going missing or ignored unnoticed. May be useful for
+local tests but not recommended for real regression tests. Together with other
+arguments, a very short running test could be started for example with:
+$ jtreg -va -nr \
+    -jdk:/path/to/latest/jdk \
+    -DtsaList=notsa "-DkeyAlgs=EC;" -DdigestAlgs=SHA-256 -Dexpired=false
+    <JDK_REPO>/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/compatibility/SignTwice.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @bug 8217375
+ * @summary This test runs those test cases of {@link Compatibility} test nearby
+ * which can be executed within the currently built and tested JDK and without
+ * TSA, with only one digest algorithm and with only one key (algorithm and
+ * size) and without delayed verification.
+ * Other test cases are to be executed manually invoking {@link Compatibility}
+ * involving more than the currently built and tested JDK verifying the
+ * compatibility of jarsigner across different JDK releases.
+ * For more details about the test and its usages, please look at the README.
+ */
+/*
+ * @test
+ * @library /test/lib ../warnings
+ * @compile Compatibility.java
+ * @run main/othervm
+ *  -Djava.security.properties=./java.security
+ *  -Duser.language=en
+ *  -Duser.country=US
+ *  -DjdkList=TEST_JDK
+ *  -DtsaList=notsa
+ *  -Dexpired=false
+ *  -DtestComprehensiveJarContents=true
+ *  -DtestJarUpdate=true
+ *  -Dstrict=true
+ *  -DkeyAlgs=EC;#RSA;#DSA;
+ *  -DdigestAlgs=SHA-512
+ *  SignTwice
+ */
+public class SignTwice {
+
+    public static void main(String[] args) throws Throwable {
+        Compatibility.main(args);
+    }
+
+}
--- a/test/jdk/sun/security/tools/jarsigner/warnings/Test.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/jdk/sun/security/tools/jarsigner/warnings/Test.java	Thu Jul 18 08:53:06 2019 +0800
@@ -61,12 +61,16 @@
     static final int VALIDITY = 365;
 
     static final String WARNING = "Warning:";
-    static final String WARNING_OR_ERROR = "(Warning|Error):";
+    static final String ERROR = "[Ee]rror:";
+    static final String WARNING_OR_ERROR = "(" + WARNING + "|" + ERROR + ")";
 
     static final String CHAIN_NOT_VALIDATED_VERIFYING_WARNING
             = "This jar contains entries "
             + "whose certificate chain is invalid.";
 
+    static final String CERTIFICATE_SELF_SIGNED
+            = "The signer's certificate is self-signed.";
+
     static final String ALIAS_NOT_IN_STORE_VERIFYING_WARNING
             = "This jar contains signed entries "
             + "that are not signed by alias in this keystore.";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/util/ManifestDigester/DigestInput.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,390 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.Attributes.Name;
+import java.util.stream.Collectors;
+
+import sun.security.util.ManifestDigester;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Factory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @compile ../../tools/jarsigner/Utils.java
+ * @run testng DigestInput
+ * @summary Checks that the manifest main attributes and entry digests are the
+ * same as before resolution of bug 8217375 which means they treat some white
+ * space different for oldStyle or digestWorkaround except for the blank line
+ * at the end of the manifest file for digestWorkaround.
+ */
+public class DigestInput {
+
+    /**
+     * Filters some test cases for calibrating expected digests with previous
+     * implementation. TODO: Delete this after calibrating with old sources.
+     */
+    static final boolean FIXED_8217375 = true; // FIXME
+
+    /**
+     * {@link ManifestDigester.Entry#digestWorkaround} should not feed the
+     * trailing blank line into the digester. Before resolution of 8217375 it
+     * fed the trailing blank line into the digest if the second line break
+     * was at the end of the file due to <pre>
+     * if (allBlank || (i == len-1)) {
+     *     if (i == len-1)
+     *         pos.endOfSection = i;
+     *     else
+     *         pos.endOfSection = last;
+     * </pre> in {@link ManifestDigester#findSection}. In that case at the end
+     * of the manifest file, {@link ManifestDigester.Entry#digestWorkaround}
+     * would have produced the same digest as
+     * {@link ManifestDigester.Entry#digest} which was wrong and without effect
+     * at best.
+     * <p>
+     * Once this fix is accepted, this flag can be removed along with
+     * {@link #assertDigestEqualsCatchWorkaroundBroken}.
+     */
+    static final boolean FIXED_8217375_EOF_ENDOFSECTION = FIXED_8217375;
+
+    static final String SECTION_NAME = "some individual section name";
+
+    @DataProvider(name = "parameters")
+    public static Object[][] parameters() {
+        List<Object[]> tests = new ArrayList<>();
+        for (String lineBreak : new String[] { "\n", "\r", "\r\n" }) {
+            if ("\r".equals(lineBreak) && !FIXED_8217375) continue;
+            for (int addLB = 0; addLB <= 4; addLB++) {
+                for (int numSecs = 0; numSecs <= 4; numSecs++) {
+                    for (boolean otherSec : new Boolean[] { false, true }) {
+                        for (boolean oldStyle : new Boolean[] { false, true }) {
+                            for (boolean workaround :
+                                    new Boolean[] { false, true }) {
+                                tests.add(new Object[] {
+                                    lineBreak, addLB, numSecs, otherSec,
+                                            oldStyle, workaround
+                                });
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return tests.toArray(new Object[tests.size()][]);
+    }
+
+    @Factory(dataProvider = "parameters")
+    public static Object[] createTests(
+            String lineBreak, int additionalLineBreaks,
+            int numberOfSections, boolean hasOtherSection,
+            boolean oldStyle, boolean digestWorkaround) {
+        return new Object[] { new DigestInput(lineBreak,
+                additionalLineBreaks, numberOfSections, hasOtherSection,
+                oldStyle, digestWorkaround)
+        };
+    }
+
+    final String lineBreak;
+    final int additionalLineBreaks; // number of blank lines delimiting section
+    final int numberOfSections;
+    final boolean hasOtherSection;
+    final boolean oldStyle;
+    final boolean digestWorkaround;
+
+    public DigestInput(
+            String lineBreak, int additionalLineBreaks,
+            int numberOfSections, boolean hasOtherSection,
+            boolean oldStyle, boolean digestWorkaround) {
+        this.lineBreak = lineBreak;
+        this.additionalLineBreaks = additionalLineBreaks;
+        this.numberOfSections = numberOfSections;
+        this.hasOtherSection = hasOtherSection;
+        this.oldStyle = oldStyle;
+        this.digestWorkaround = digestWorkaround;
+    }
+
+    @BeforeMethod
+    public void verbose() {
+        System.out.println("-".repeat(72));
+        System.out.println("lineBreak = " +
+                Utils.escapeStringWithNumbers(lineBreak));
+        System.out.println("additionalLineBreaks = " + additionalLineBreaks);
+        System.out.println("numberOfSections = " + numberOfSections);
+        System.out.println("hasOtherSection = " + hasOtherSection);
+        System.out.println("oldStyle = " + oldStyle);
+        System.out.println("digestWorkaround = " + digestWorkaround);
+        System.out.println("-".repeat(72));
+    }
+
+    byte[] rawManifestBytes() {
+        return (
+            Name.MANIFEST_VERSION + ": 1.0" + lineBreak +
+            "OldStyle0: no trailing space" + lineBreak +
+            "OldStyle1: trailing space " + lineBreak +
+            "OldStyle2: two trailing spaces  " + lineBreak +
+                    lineBreak.repeat(additionalLineBreaks) +
+            (
+                "Name: " + SECTION_NAME + lineBreak +
+                "OldStyle0: no trailing space" + lineBreak +
+                "OldStyle1: trailing space " + lineBreak +
+                "OldStyle2: two trailing spaces  " + lineBreak +
+                lineBreak.repeat(additionalLineBreaks)
+            ).repeat(numberOfSections) +
+            (hasOtherSection ?
+                "Name: unrelated trailing section" + lineBreak +
+                "OldStyle0: no trailing space" + lineBreak +
+                "OldStyle1: trailing space " + lineBreak +
+                "OldStyle2: two trailing spaces  " + lineBreak +
+                lineBreak.repeat(additionalLineBreaks)
+            : "")
+        ).getBytes(UTF_8);
+    }
+
+    byte[] expectedMainAttrsDigest(boolean digestWorkaround) {
+        return (
+            Name.MANIFEST_VERSION + ": 1.0" + lineBreak +
+            "OldStyle0: no trailing space" + lineBreak +
+            "OldStyle1: trailing space" +
+                (!oldStyle || !lineBreak.startsWith("\r") || digestWorkaround ?
+                    " " : "") + lineBreak +
+            "OldStyle2: two trailing spaces " +
+                (!oldStyle || !lineBreak.startsWith("\r") || digestWorkaround ?
+                        " " : "") + lineBreak +
+            (
+                     (
+                                 !digestWorkaround
+                         || (
+                                 additionalLineBreaks == 1
+                              && numberOfSections == 0
+                              && !hasOtherSection
+                              && (
+                                      digestWorkaround
+                                   && !FIXED_8217375_EOF_ENDOFSECTION
+                                 )
+                         )
+                ) && (
+                            additionalLineBreaks > 0
+                         || numberOfSections > 0
+                         || hasOtherSection
+                )
+            ? lineBreak : "")
+        ).getBytes(UTF_8);
+    }
+
+    byte[] expectedIndividualSectionDigest(boolean digestWorkaround) {
+        if (numberOfSections == 0) return null;
+        return (
+            (
+                "Name: " + SECTION_NAME + lineBreak +
+                "OldStyle0: no trailing space" + lineBreak +
+                "OldStyle1: trailing space" +
+                    (!oldStyle || !lineBreak.startsWith("\r")
+                            || digestWorkaround ? " " : "") + lineBreak +
+                "OldStyle2: two trailing spaces " +
+                    (!oldStyle || !lineBreak.startsWith("\r")
+                            || digestWorkaround ? " " : "") + lineBreak +
+                (
+                    (
+                           !digestWorkaround
+                    ) && (
+                           additionalLineBreaks > 0
+                    )
+                ? lineBreak : "")
+            ).repeat(numberOfSections) +
+            (
+                   additionalLineBreaks == 1
+                && !hasOtherSection
+                && digestWorkaround
+                && !FIXED_8217375_EOF_ENDOFSECTION
+            ? lineBreak : "")
+        ).getBytes(UTF_8);
+    }
+
+    class EchoMessageDigest extends MessageDigest {
+
+        ByteArrayOutputStream buf;
+
+        EchoMessageDigest() {
+            super("echo");
+        }
+
+        @Override
+        protected void engineReset() {
+            buf = new ByteArrayOutputStream();
+        }
+
+        @Override
+        protected void engineUpdate(byte input) {
+            buf.write(input);
+        }
+
+        @Override
+        protected void engineUpdate(byte[] i, int o, int l) {
+            buf.write(i, o, l);
+        }
+
+        @Override protected byte[] engineDigest() {
+            return buf.toByteArray();
+        }
+
+    }
+
+    byte[] digestMainAttributes(byte[] mfBytes) throws Exception {
+        Utils.echoManifest(mfBytes, "going to digest main attributes of");
+
+        ManifestDigester md = new ManifestDigester(mfBytes);
+        ManifestDigester.Entry entry =
+                md.get(ManifestDigester.MF_MAIN_ATTRS, oldStyle);
+        MessageDigest digester = new EchoMessageDigest();
+        return digestWorkaround ?
+                entry.digestWorkaround(digester) : entry.digest(digester);
+    }
+
+    byte[] digestIndividualSection(byte[] mfBytes) throws Exception {
+        Utils.echoManifest(mfBytes,
+                "going to digest section " + SECTION_NAME + " of");
+
+        ManifestDigester md = new ManifestDigester(mfBytes);
+        ManifestDigester.Entry entry = md.get(SECTION_NAME, oldStyle);
+        if (entry == null) {
+            return null;
+        }
+        MessageDigest digester = new EchoMessageDigest();
+        return digestWorkaround ?
+                entry.digestWorkaround(digester) : entry.digest(digester);
+    }
+
+
+    /**
+     * Checks that the manifest main attributes digest is the same as before.
+     */
+    @Test
+    public void testMainAttributesDigest() throws Exception {
+        byte[] mfRaw = rawManifestBytes();
+        byte[] digest = digestMainAttributes(mfRaw);
+        byte[] expectedDigest = expectedMainAttrsDigest(digestWorkaround);
+
+        // the individual section will be digested along with the main
+        // attributes if not properly delimited with a blank line
+        if (additionalLineBreaks == 0
+                && (numberOfSections > 0 || hasOtherSection)) {
+            assertNotEquals(digest, expectedDigest);
+            return;
+        }
+
+        byte[] expectedDigestNoWorkaround = expectedMainAttrsDigest(false);
+
+//        assertDigestEquals(digest, expectedDigest); // FIXME
+        assertDigestEqualsCatchWorkaroundBroken(
+                digest, expectedDigest, expectedDigestNoWorkaround);
+    }
+
+    /**
+     * Checks that an individual section digest is the same as before.
+     */
+    @Test
+    public void testIndividualSectionDigest() throws Exception {
+        byte[] mfRaw = rawManifestBytes();
+        byte[] digest = digestIndividualSection(mfRaw);
+
+        // no digest will be produced for an individual section that is not
+        // properly section delimited with a blank line.
+        byte[] expectedDigest =
+                additionalLineBreaks == 0 ? null :
+                    expectedIndividualSectionDigest(digestWorkaround);
+
+        byte[] expectedDigestNoWorkaround =
+                additionalLineBreaks == 0 ? null :
+                    expectedIndividualSectionDigest(false);
+
+//      assertDigestEquals(digest, expectedDigest); // FIXME
+        assertDigestEqualsCatchWorkaroundBroken(
+                digest, expectedDigest, expectedDigestNoWorkaround);
+    }
+
+    static int firstDiffPos = Integer.MAX_VALUE;
+
+    /**
+     * @see FIXED_8217375_EOF_ENDOFSECTION
+     */
+    void assertDigestEqualsCatchWorkaroundBroken(
+            byte[] actual, byte[] expected, byte[] expectedNoWorkaround)
+                    throws IOException {
+        try {
+            assertDigestEquals(actual, expected);
+        } catch (AssertionError e) {
+            if (digestWorkaround && FIXED_8217375_EOF_ENDOFSECTION &&
+                    Arrays.equals(expected, expectedNoWorkaround)) {
+                // if digests with and without workaround are the same anyway
+                // the workaround has failed and could not have worked with
+                // the same digest as produced without workaround before
+                // which would not match either because equal.
+                return;
+            }
+            fail("failed also without digestWorkaound", e);
+        }
+    }
+
+    void assertDigestEquals(byte[] actual, byte[] expected) throws IOException {
+        if (actual == null && expected == null) return;
+        Utils.echoManifest(actual, "actual digest");
+        Utils.echoManifest(expected, "expected digest");
+        for (int i = 0; i < actual.length && i < expected.length; i++) {
+            if (actual[i] != expected[i]) {
+                firstDiffPos = Math.min(firstDiffPos, i);
+                verbose();
+                fail("found first difference in current test"
+                        + " at position " + i);
+            }
+        }
+        if (actual.length != expected.length) {
+            int diffPos = Math.min(actual.length, expected.length);
+            firstDiffPos = Math.min(firstDiffPos, diffPos);
+            verbose();
+            fail("found first difference in current test"
+                    + " at position " + diffPos + " after one digest end");
+        }
+        assertEquals(actual, expected);
+    }
+
+    @AfterTest
+    public void reportFirstDiffPos() {
+        System.err.println("found first difference in all tests"
+                + " at position " + firstDiffPos);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/util/ManifestDigester/FindSection.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,750 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+
+import sun.security.util.ManifestDigester;
+
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Factory;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util:+open
+ * @compile ../../tools/jarsigner/Utils.java
+ * @run testng/othervm FindSection
+ * @summary Check {@link ManifestDigester#findSection}.
+ */
+public class FindSection {
+
+    /*
+     * TODO:
+     * FIXED_8217375 is not intended to keep. it is intended to show what
+     * exactly has changed with respect to the previous version for which no
+     * such test existed.
+     */
+    static final boolean FIXED_8217375 = true;
+
+    /**
+     * {@link ManifestDigester.Entry#digestWorkaround} should not feed the
+     * trailing blank line into the digester. Before resolution of 8217375 it
+     * fed the trailing blank line into the digest if the second line break
+     * was at the end of the file due to <pre>
+     * if (allBlank || (i == len-1)) {
+     *     if (i == len-1)
+     *         pos.endOfSection = i;
+     *     else
+     *         pos.endOfSection = last;
+     * </pre> in {@link ManifestDigester#findSection}. In that case at the end
+     * of the manifest file, {@link ManifestDigester.Entry#digestWorkaround}
+     * would have produced the same digest as
+     * {@link ManifestDigester.Entry#digest} which was wrong and without effect
+     * at best.
+     * <p>
+     * Once this fix is accepted, this flag can be removed along with
+     * {@link #actualEndOfSection8217375}.
+     */
+    static final boolean FIXED_8217375_EOF_ENDOFSECTION = FIXED_8217375;
+
+    /**
+     * {@link ManifestDigester.Position.endOfSection} usually points to the
+     * start position of the blank line trailing a section minus one.
+     * If a {@link ManifestDigester.Position} returned by
+     * {@link ManifestDigester#findSection} is based on a portion that starts
+     * with a blank line, above statement is (or was) not true, because of the
+     * initialization of {@code last} in {@link ManifestDigester#findSection}
+     * <pre>
+     * int last = offset;
+     * </pre>
+     * which would point after incrementing it in {@code pos.endOfSection + 1}
+     * on line 128 (line number before this change) or {@code int sectionLen =
+     * pos.endOfSection-start+1;} on line 133 (line number before this change)
+     * at one byte after the first line break character of usually two and
+     * possibly (assuming "{@code \r\n}" default line break normally) in between
+     * the two characters of a line break. After subtracting again the index of
+     * the section start position on former line 133, the last byte would be
+     * missed to be digested by {@link ManifestDigester.Entry#digestWorkaround}.
+     * <p>
+     * All this, however could possibly matter (or have mattered) only when
+     * {@link ManifestDigester#findSection} was invoked with an offset position
+     * pointing straight to a line break which happens if a manifest starts
+     * with an empty line or if there are superfluous blank lines between
+     * sections in both cases no useful manifest portion is identified.
+     * Superfluous blank lines are not identified as sections (because they
+     * don't have a name and specifically don't meet {@code if (len > 6) {} on
+     * former line 136. Manifests starting with a line break are not any more
+     * useful either.
+     * <p>
+     * Once this fix is accepted, this flag can be removed along with
+     * {@link #actualEndOfSection8217375}.
+     */
+    static final boolean FIXED_8217375_STARTWITHBLANKLINE_ENDOFSECTION =
+            FIXED_8217375;
+
+    static Constructor<?> PositionConstructor;
+    static Method findSection;
+    static Field rawBytes;
+    static Field endOfFirstLine;
+    static Field endOfSection;
+    static Field startOfNext;
+
+    @BeforeClass
+    public static void setFindSectionAccessible() throws Exception {
+        Class<?> Position = Arrays.stream(ManifestDigester.class.
+                getDeclaredClasses()).filter(c -> c.getSimpleName().
+                        equals("Position")).findFirst().get();
+        PositionConstructor = Position.getDeclaredConstructor();
+        PositionConstructor.setAccessible(true);
+        findSection = ManifestDigester.class.getDeclaredMethod("findSection",
+            int.class, Position);
+        findSection.setAccessible(true);
+        rawBytes = ManifestDigester.class.getDeclaredField("rawBytes");
+        rawBytes.setAccessible(true);
+        endOfFirstLine = Position.getDeclaredField("endOfFirstLine");
+        endOfFirstLine.setAccessible(true);
+        endOfSection = Position.getDeclaredField("endOfSection");
+        endOfSection.setAccessible(true);
+        startOfNext = Position.getDeclaredField("startOfNext");
+        startOfNext.setAccessible(true);
+    }
+
+    static class Position {
+        final int endOfFirstLine; // not including newline character
+
+        final int endOfSection; // end of section, not including the blank line
+                                // between sections
+        final int startOfNext;  // the start of the next section
+
+        Position(Object pos) throws ReflectiveOperationException {
+            endOfFirstLine = FindSection.endOfFirstLine.getInt(pos);
+            endOfSection = FindSection.endOfSection.getInt(pos);
+            startOfNext = FindSection.startOfNext.getInt(pos);
+        }
+    }
+
+    Position findSection(byte[] manifestBytes)
+            throws ReflectiveOperationException {
+        ManifestDigester manDig = new ManifestDigester("\n\n".getBytes(UTF_8));
+        FindSection.rawBytes.set(manDig, manifestBytes);
+        Object pos = PositionConstructor.newInstance();
+        Object result = findSection.invoke(manDig, offset, pos);
+        if (Boolean.FALSE.equals(result)) {
+            return null; // indicates findSection having returned false
+        } else {
+            return new Position(pos);
+        }
+    }
+
+    @DataProvider(name = "parameters")
+    public static Object[][] parameters() {
+        return new Object[][] { { 0 }, { 42 } };
+    }
+
+    @Factory(dataProvider = "parameters")
+    public static Object[] createTests(int offset) {
+        return new Object[]{ new FindSection(offset) };
+    }
+
+    final int offset;
+
+    FindSection(int offset) {
+        this.offset = offset;
+    }
+
+    @BeforeMethod
+    public void verbose() {
+        System.out.println("offset = " + offset);
+    }
+
+    Position findSection(String manifestString)
+            throws ReflectiveOperationException {
+        byte[] manifestBytes = manifestString.getBytes(UTF_8);
+        byte[] manifestWithOffset = new byte[manifestBytes.length + offset];
+        System.arraycopy(manifestBytes, 0, manifestWithOffset, offset,
+                manifestBytes.length);
+        return findSection(manifestWithOffset);
+    }
+
+    /**
+     * Surprising, but the offset actually makes a difference in
+     * {@link ManifestDigester#findSection} return value.
+     */
+    @SuppressWarnings("unused")
+    int actualEndOfFirstLine8217375(int correctPosition) {
+        // if the parsed portion of the manifest starts with a blank line,
+        // and offset is 0, "pos.endOfFirstLine = -1;" probably denoting a
+        // yet uninitialized value coincides with the assignment by
+        // "pos.endOfFirstLine = i-1;" if i == 0 and
+        // "if (pos.endOfFirstLine == -1)" after "case '\n':" happens to
+        // become true even though already assigned.
+        if (offset == 0 && correctPosition == -1 && !FIXED_8217375) return 0;
+        return correctPosition;
+    }
+
+    @SuppressWarnings("unused")
+    int actualEndOfSection8217375(int correctPosition, boolean eof, int lbl) {
+        // if the parsed portion of the manifest ends with a blank line and
+        // just before eof, the blank line is included in Position.endOfSection/
+        // Section.length (the one usually without blank line as well as in
+        // Position.startOfNext/Section.lengthWithBlankLine) which is used
+        // in digestWorkaround (independent of the digest without workaround)
+        if (eof && !FIXED_8217375_EOF_ENDOFSECTION) {
+            return correctPosition + lbl;
+        } else if (correctPosition == -1
+                && !FIXED_8217375_STARTWITHBLANKLINE_ENDOFSECTION) {
+            return 0;
+        } else {
+            return correctPosition;
+        }
+    }
+
+    AssertionError collectErrors(AssertionError a, Runnable run) {
+        try {
+            run.run();
+        } catch (AssertionError e) {
+            if (a == null) a = new AssertionError();
+            a.addSuppressed(e);
+        }
+        return a;
+    }
+
+    void assertPosition(Position pos,
+            int endOfFirstLine, int endOfSection, int startOfNext) {
+        AssertionError a = null;
+        a = collectErrors(a, () -> assertEquals(
+                pos.endOfFirstLine, endOfFirstLine + offset, "endOfFirstLine"));
+        a = collectErrors(a, () -> assertEquals(
+                pos.endOfSection, endOfSection + offset, "endOfSection"));
+        a = collectErrors(a, () -> assertEquals(
+                pos.startOfNext, startOfNext + offset, "startOfNext"));
+        if (a != null) throw a;
+    }
+
+    void catchCrCausesIndexOutOfBoundsException(
+            Callable<Position> test, Consumer<Position> asserts) {
+        try {
+            Position x = test.call();
+            if (!FIXED_8217375) fail();
+            asserts.accept(x);
+        } catch (Exception e) {
+            if (e instanceof IndexOutOfBoundsException ||
+                e.getCause() instanceof IndexOutOfBoundsException) {
+                if (FIXED_8217375) throw new AssertionError(e);
+            } else {
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    @Test
+    public void testEmpty() throws Exception {
+        assertNull(findSection(""));
+    }
+
+    @Test
+    public void testOneLineBreakCr() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("\r"),
+                p -> assertPosition(p,
+                        -1, actualEndOfSection8217375(-1, false, 1), 1)
+        );
+    }
+
+    @Test
+    public void testOneLineBreakLf() throws Exception {
+        assertPosition(findSection("\n"),
+                -1, actualEndOfSection8217375(-1, false, 1), 1);
+    }
+
+    @Test
+    public void testOneLineBreakCrLf() throws Exception {
+        assertPosition(findSection("\r\n"),
+                actualEndOfFirstLine8217375(-1),
+                actualEndOfSection8217375(-1, true, 2),
+                2);
+    }
+
+    @Test
+    public void testSpaceAndLineBreakCr() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("   \r"),
+                p -> assertPosition(p, 2, 3, 4)
+        );
+    }
+
+    @Test
+    public void testSpaceAndOneLineBreakLf() throws Exception {
+        assertPosition(findSection("   \n"), 2, 3, 4);
+    }
+
+    @Test
+    public void testSpaceAndOneLineBreakCrLf() throws Exception {
+        assertPosition(findSection("   \r\n"), 2, 4, 5);
+    }
+
+    @Test
+    public void testOneLineBreakCrAndSpace() throws Exception {
+        assertPosition(findSection("\r   "),
+                -1, actualEndOfSection8217375(-1, false, 1), 1);
+    }
+
+    @Test
+    public void testOneLineBreakLfAndSpace() throws Exception {
+        assertPosition(findSection("\n   "),
+                -1, actualEndOfSection8217375(-1, false, 1), 1);
+    }
+
+    @Test
+    public void testOneLineBreakCrLfAndSpace() throws Exception {
+        assertPosition(findSection("\r\n   "),
+                actualEndOfFirstLine8217375(-1),
+                actualEndOfSection8217375(-1, false, 1),
+                2);
+    }
+
+    @Test
+    public void testCrEof() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("abc\r"),
+                p -> assertPosition(p, 2, 3, 4)
+        );
+    }
+
+    @Test
+    public void testLfEof() throws Exception {
+        assertPosition(findSection("abc\n"), 2, 3, 4);
+    }
+
+    @Test
+    public void testCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\n"), 2, 4, 5);
+    }
+
+    @Test
+    public void testCrContinued() throws Exception {
+        assertPosition(findSection("abc\rxyz\r\n\r\n   "), 2, 8, 11);
+    }
+
+    @Test
+    public void testLfContinued() throws Exception {
+        assertPosition(findSection("abc\nxyz\r\n\r\n   "), 2, 8, 11);
+    }
+
+    @Test
+    public void testCrLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrCrEof() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("abc\r\nxyz\r\r"),
+                p -> assertPosition(p,
+                        2, actualEndOfSection8217375(8, true, 1), 10)
+        );
+    }
+
+    @Test
+    public void testCrCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\r   "), 2, 8, 10);
+    }
+
+    @Test
+    public void testLfLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\n\n"),
+                2, actualEndOfSection8217375(8, true, 1), 10);
+    }
+
+    @Test
+    public void testLfLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\n\n   "), 2, 8, 10);
+    }
+
+    @Test
+    public void testCrLfEof2() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n"), 2, 9, 10);
+    }
+
+    @Test
+    public void testMainSectionNotTerminatedWithLineBreak() throws Exception {
+        assertNull(findSection("abc\r\nxyz\r\n   "));
+    }
+
+    @Test
+    public void testLfCrEof() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("abc\r\nxyz\n\r"),
+                p -> assertPosition(p,
+                        2, actualEndOfSection8217375(8, true, 1), 10)
+        );
+    }
+
+    @Test
+    public void testLfCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\n\r   "), 2, 8, 10);
+    }
+
+    @Test
+    public void testCrLfCrEof() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("abc\r\nxyz\r\n\r"),
+                p -> assertPosition(p,
+                        2, actualEndOfSection8217375(9, true, 2), 11)
+        );
+    }
+
+    @Test
+    public void testCrLfCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n"),
+                2, actualEndOfSection8217375(9, true, 1), 11);
+    }
+
+    @Test
+    public void testCrLfLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n"),
+                2, actualEndOfSection8217375(9, true, 2), 12);
+    }
+
+    @Test
+    public void testCrLfCfLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCrCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrLfCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r"), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCfLfCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCrLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfLfLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\n   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrLfLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\n   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCrCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n   "), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrLfCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\n"), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCfLfCrLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\n   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfLfCrCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrLfCrCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\r"), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCrLfCrContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfLfLfCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\n\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrLfCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfLfLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\n\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrLfLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\r\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrLfCrCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\r\n"), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfCrLfCrLfContinued() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\n   "), 2, 9, 12);
+    }
+
+    @Test
+    public void testCrLfLfLfCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\n\r\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfLfCrLfCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n\r\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrLfCrCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrCrCrEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\r"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrLfLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrLfCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n\r\n"), 2, 9, 11);
+    }
+
+    @Test
+    public void testCrLfCrCrCrLfEof() throws Exception {
+        assertPosition(findSection("abc\r\nxyz\r\n\r\r\r\n"), 2, 9, 11);
+    }
+
+    /*
+     * endOfFirstLine is the same regardless of the line break delimiter
+     */
+    @Test
+    public void testEndOfFirstLineVsLineBreak() throws Exception {
+        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
+            Position p = findSection("abc" + lb + "xyz" + lb + lb + " ");
+
+            // main assertion showing endOfFirstLine independent of line break
+            assertEquals(p.endOfFirstLine, 2 + offset);
+
+            // assert remaining positions as well just for completeness
+            assertPosition(p, 2, 5 + 2 * lb.length(), 6 + 3 * lb.length());
+        }
+    }
+
+    /*
+     * '\r' at the end of the bytes causes index out of bounds exception
+     */
+    @Test
+    public void testCrLastCausesIndexOutOfBounds() throws Exception {
+        catchCrCausesIndexOutOfBoundsException(
+                () -> findSection("\r"),
+                p -> assertPosition(p,
+                        -1, actualEndOfSection8217375(-1, true, 1), 1)
+        );
+    }
+
+    /*
+     * endOfSection includes second line break if at end of bytes only
+     */
+    @Test
+    public void testEndOfSectionWithLineBreakVsEof() throws Exception {
+        AssertionError errors = new AssertionError("offset = " + offset);
+        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
+            for (boolean eof : new boolean[] { false, true }) {
+                Position p;
+                try {
+                    p = findSection("abc" + lb + lb + (eof ? "" : "xyz"));
+                } catch (RuntimeException | ReflectiveOperationException e) {
+                    if ((e instanceof IndexOutOfBoundsException ||
+                         e.getCause() instanceof IndexOutOfBoundsException)
+                          && eof && "\r".equals(lb) && !FIXED_8217375) continue;
+                    throw e;
+                }
+
+                AssertionError a = new AssertionError("offset = " + offset
+                        + ", lb = " + Utils.escapeStringWithNumbers(lb) + ", "
+                        + "eof = " + eof);
+
+                // main assertion showing endOfSection including second line
+                // break when at end of file
+                a = collectErrors(a, () -> assertEquals(
+                        p.endOfSection,
+                        actualEndOfSection8217375(
+                                2 + lb.length() + offset, eof, lb.length()) ));
+
+                // assert remaining positions as well just for completeness
+                a = collectErrors(a, () -> assertPosition(p,
+                        2,
+                        actualEndOfSection8217375(
+                                2 + lb.length(), eof, lb.length()),
+                        3 + lb.length() * 2));
+
+                if (a.getSuppressed().length > 0) errors.addSuppressed(a);
+            }
+        }
+        if (errors.getSuppressed().length > 0) throw errors;
+    }
+
+    /*
+     * returns position even if only one line break before end of bytes.
+     * because no name will be found the result will be skipped and no entry
+     * will be created.
+     */
+    @Test
+    public void testReturnPosVsEof() throws Exception {
+        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
+            for (boolean eof : new boolean[] { false, true }) {
+                try {
+                    Position p = findSection("abc" + lb + (eof ? "" : "xyz"));
+                    assertTrue(p != null == eof);
+                } catch (RuntimeException | ReflectiveOperationException e) {
+                    if ((e instanceof IndexOutOfBoundsException ||
+                         e.getCause() instanceof IndexOutOfBoundsException)
+                          && eof && "\r".equals(lb) && !FIXED_8217375) continue;
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /*
+     * it could be normally be expected that startOfNext would point to the
+     * start of the next section after a blank line but that is not the case
+     * if a section ends with only one line break and no blank line immediately
+     * before eof of the manifest.
+     * such an entry will be digested without the trailing blank line which is
+     * only fine until another section should be added afterwards.
+     */
+    @Test
+    public void testStartOfNextPointsToEofWithNoBlankLine() throws Exception {
+        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
+            for (boolean blank : new boolean[] { false, true }) {
+                String manifest = "abc" + lb + "xyz" + lb + (blank ? lb : "");
+                try {
+                    Position p = findSection(manifest);
+
+                    // assert that startOfNext points to eof in all cases
+                    // whether with or without a blank line before eof
+                    assertEquals(p.startOfNext, manifest.length() + offset);
+
+                    // assert remaining positions as well just for completeness
+                    assertPosition(p,
+                            2,
+                            actualEndOfSection8217375(
+                                    5 + lb.length() * 2,
+                                    true,
+                                    blank ? lb.length() : 0),
+                            manifest.length());
+                } catch (RuntimeException | ReflectiveOperationException e) {
+                    if ((e instanceof IndexOutOfBoundsException ||
+                         e.getCause() instanceof IndexOutOfBoundsException)
+                          && "\r".equals(lb) && !FIXED_8217375) continue;
+                    throw e;
+                }
+            }
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/util/ManifestDigester/FindSections.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import sun.security.util.ManifestDigester;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @run testng FindSections
+ * @summary Check {@link ManifestDigester#ManifestDigester} processing
+ * individual sections and particularly identifying their names correctly.
+ * Main attributes are not covered in this test.
+ * <p>
+ * See also {@link FindSection} for the {@link ManifestDigester#findSection}
+ * method specifically.
+ */
+public class FindSections {
+
+    static final String DEFAULT_MANIFEST = "Manifest-Version: 1.0\r\n\r\n";
+
+    void test(String manifest, String expectedSection) {
+        ManifestDigester md = new ManifestDigester(manifest.getBytes(UTF_8));
+        if (expectedSection != null) {
+            assertNotNull(md.get(expectedSection, false));
+        } else {
+            assertNull(md.get(expectedSection, false));
+        }
+    }
+
+    @Test
+    public void testNameNoSpaceAfterColon() throws Exception {
+        test(DEFAULT_MANIFEST + "Name:section\r\n\r\n", null);
+    }
+
+    @Test
+    public void testNameCase() throws Exception {
+        test(DEFAULT_MANIFEST + "nAME: section\r\n\r\n", "section");
+    }
+
+    @Test
+    public void testEmptyName() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: \r\n\r\n", "");
+    }
+
+    @Test
+    public void testShortestInvalidSection() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: ", null);
+    }
+
+    @Test
+    public void testMinimalValidSection() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: \r", "");
+    }
+
+    @Test
+    public void testNameNotContinued() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: FooBar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testImmediatelyContinuedCrName() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: \r FooBar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testImmediatelyContinuedLfName() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: \n FooBar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testImmediatelyContinuedCrLfName() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: \r\n FooBar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedCr() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: FooBar\r \r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedLf() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: FooBar\n \r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedCrLf() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: FooBar\r\n \r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedCrIgnoreNextChar() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Foo\r: Bar\r\n", "Foo");
+    }
+
+    @Test
+    public void testNameContinuedCrIgnoreNextCharSpace() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Foo\r  Bar\r\n", "Foo Bar");
+    }
+
+    @Test
+    public void testNameContinuedContinuedCr() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Fo\r\n oB\r ar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedContinuedLf() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Fo\r\n oB\n ar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedContinuedCrLf() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Fo\r\n oB\r\n ar\r\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedEndCr() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Foo\r\n Bar\r", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedEndLf() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Foo\r\n Bar\n", "FooBar");
+    }
+
+    @Test
+    public void testNameContinuedEndCrLf() throws Exception {
+        test(DEFAULT_MANIFEST + "Name: Foo\r\n Bar\r\n", "FooBar");
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/util/ManifestDigester/LineBreaks.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.function.Function;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import sun.security.util.ManifestDigester;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @compile ../../tools/jarsigner/Utils.java
+ * @run testng LineBreaks
+ * @summary Verify {@code ManifestDigester} reads different line breaks well.
+ * The specifications state:
+ * <q><pre>newline: CR LF | LF | CR (not followed by LF)</pre></q>.
+ * This test does not verify that the digests are correct.
+ */
+public class LineBreaks {
+
+    static final String KEY = "Key";
+    static final String VALUE = "Value";
+    static final String SECTION = "Section";
+    static final String FOO = "Foo";
+    static final String BAR = "Bar";
+
+    static final String EXCEED_LINE_WIDTH_LIMIT = "x".repeat(71);
+
+    String breakAndContinue(String str, String lineBreak) {
+        // assert no multi-byte UTF-8 encoded characters in this test
+        assertEquals(str.getBytes(UTF_8).length, str.length());
+
+        int p = 1;
+        while (p + 71 < str.length()) {
+            p += 71;
+            str = str.substring(0, p) + lineBreak + " " + str.substring(p);
+            p += lineBreak.length() + 1;
+        }
+        return str;
+    }
+
+    byte[] createTestManifest(String lineBreak, boolean onlyMainAttrs,
+            String excess) throws IOException {
+        System.out.println("lineBreak = "
+                + Utils.escapeStringWithNumbers(lineBreak));
+        System.out.println("onlyMainAttrs = " + onlyMainAttrs);
+        String mf = "";
+        mf += breakAndContinue(
+                KEY + ": " + VALUE + excess, lineBreak) + lineBreak;
+        mf += lineBreak;
+        if (!onlyMainAttrs) {
+            mf += breakAndContinue(
+                    "Name: " + SECTION + excess, lineBreak) + lineBreak;
+            mf += breakAndContinue(
+                    FOO + ": " + BAR + excess, lineBreak) + lineBreak;
+            mf += lineBreak;
+        }
+        byte[] mfBytes = mf.getBytes(UTF_8);
+        Utils.echoManifest(mfBytes, "binary manifest");
+        return mfBytes;
+    }
+
+    @DataProvider(name = "parameters")
+    public static Object[][] parameters() {
+        List<Object[]> tests = new ArrayList<>();
+        for (String lineBreak : new String[] { "\n", "\r", "\r\n" }) {
+            for (boolean onlyMainAttrs : new boolean[] { false, true }) {
+                for (int numLbs = 0; numLbs < 3; numLbs++) {
+                    tests.add(new Object[]{ lineBreak, onlyMainAttrs, numLbs });
+                }
+            }
+        }
+        return tests.toArray(new Object[tests.size()][]);
+    }
+
+    @Test(dataProvider = "parameters")
+    public void test(String lineBreak, boolean onlyMainAttrs, int numLbs)
+            throws IOException {
+        String excess = EXCEED_LINE_WIDTH_LIMIT.repeat(numLbs);
+        byte[] mfBytes = createTestManifest(lineBreak, onlyMainAttrs, excess);
+
+        // self-test: make sure the manifest is valid and represents the
+        // values as expected before attempting to digest it
+        Manifest mf = new Manifest(new ByteArrayInputStream(mfBytes));
+        assertEquals(mf.getMainAttributes().getValue(KEY), VALUE + excess);
+        Attributes section = mf.getAttributes(SECTION + excess);
+        if (onlyMainAttrs) {
+            assertNull(section);
+        } else {
+            assertEquals(section.getValue(FOO), BAR + excess);
+        }
+
+        // verify that ManifestDigester has actually found the individual
+        // section if and only if it was present thereby also implying based
+        // on ManifestDigester implementation that the main attributes were
+        // found before
+        ManifestDigester md = new ManifestDigester(mfBytes);
+        assertTrue((md.get(SECTION + excess, false) != null) != onlyMainAttrs);
+    }
+
+    static List<Integer> stringToIntList(String string) {
+        byte[] bytes = string.getBytes(UTF_8);
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < bytes.length; i++) {
+            list.add((int) bytes[i]);
+        }
+        return list;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/util/ManifestDigester/ReproduceRaw.java	Thu Jul 18 08:53:06 2019 +0800
@@ -0,0 +1,324 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.jar.Manifest;
+
+import sun.security.util.ManifestDigester;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Factory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @modules java.base/sun.security.util
+ * @compile ../../tools/jarsigner/Utils.java
+ * @run testng ReproduceRaw
+ * @summary Verifies that {@link ManifestDigester} can reproduce parts of
+ * manifests in their binary form so that {@link JarSigner} can rely on
+ * {@link ManifestDigester.Entry#reproduceRaw} to write in a map view
+ * unmodified entries back also unmodified in their binary form.
+ * <p>
+ * See also<ul>
+ * <li>{@link PreserveRawManifestEntryAndDigest} with end to end tests
+ * with {@code jarsigner} tool and</li>
+ * <li>{@link FindHeaderEndVsManifestDigesterFindFirstSection} about
+ * identifying the binary portion of only main attributes and more extensive
+ * main attributes digesting tests while this one test here is more about
+ * reproducing individual sections and that they result in the same
+ * digests.</li>
+ * </ul>
+ */
+public class ReproduceRaw {
+
+    static final boolean VERBOSE = false;
+
+    @DataProvider(name = "parameters")
+    public static Object[][] parameters() {
+        List<Object[]> tests = new ArrayList<>();
+        for (String lineBreak : new String[] { "\n", "\r", "\r\n" }) {
+            for (boolean oldStyle : new Boolean[] { false, true }) {
+                for (boolean workaround : new Boolean[] { false, true }) {
+                    tests.add(new Object[] { lineBreak, oldStyle, workaround });
+                }
+            }
+        }
+        return tests.toArray(new Object[tests.size()][]);
+    }
+
+    @Factory(dataProvider = "parameters")
+    public static Object[] createTests(String lineBreak,
+            boolean oldStyle, boolean digestWorkaround) {
+        return new Object[]{
+                new ReproduceRaw(lineBreak, oldStyle, digestWorkaround)
+        };
+    }
+
+    final String lineBreak;
+    final boolean oldStyle;
+    final boolean digestWorkaround;
+
+    public ReproduceRaw(String lineBreak,
+            boolean oldStyle, boolean digestWorkaround) {
+        this.lineBreak = lineBreak;
+        this.oldStyle = oldStyle;
+        this.digestWorkaround = digestWorkaround;
+    }
+
+    @BeforeMethod
+    public void verbose() {
+        System.out.println("lineBreak = " +
+                Utils.escapeStringWithNumbers(lineBreak));
+        System.out.println("oldStyle = " + oldStyle);
+        System.out.println("digestWorkaround = " + digestWorkaround);
+    }
+
+    class EchoMessageDigest extends MessageDigest {
+
+        ByteArrayOutputStream buf;
+
+        EchoMessageDigest() {
+            super("echo");
+        }
+
+        @Override
+        protected void engineReset() {
+            buf = new ByteArrayOutputStream();
+        }
+
+        @Override
+        protected void engineUpdate(byte input) {
+            buf.write(input);
+        }
+
+        @Override
+        protected void engineUpdate(byte[] i, int o, int l) {
+            buf.write(i, o, l);
+        }
+
+        @Override protected byte[] engineDigest() {
+            return buf.toByteArray();
+        }
+
+    }
+
+    /**
+     * similar to corresponding part of {@link JarSigner#sign0}
+     * (stripped down to the code for reproducing the old manifest entry by
+     * entry which was too difficult to achieve using the real JarSigner code
+     * in the test here)
+     */
+    byte[] reproduceRawManifest(byte[] mfRawBytes,
+            boolean mainAttsProperlyDelimited,
+            boolean sectionProperlyDelimited) throws IOException {
+        Manifest manifest = new Manifest(new ByteArrayInputStream(mfRawBytes));
+        ManifestDigester oldMd = new ManifestDigester(mfRawBytes);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        // main attributes
+        assertEquals(oldMd.getMainAttsEntry().isProperlyDelimited(),
+                mainAttsProperlyDelimited);
+        oldMd.getMainAttsEntry().reproduceRaw(baos);
+
+        // individual sections
+        for (String key : manifest.getEntries().keySet()) {
+            assertEquals(oldMd.get(key).isProperlyDelimited(),
+                    sectionProperlyDelimited);
+            oldMd.get(key).reproduceRaw(baos);
+        }
+
+        return baos.toByteArray();
+    }
+
+    static String regExscape(String expr) {
+        for (int i = 0; i < expr.length(); i++) {
+            if (expr.charAt(i) == '\r' || expr.charAt(i) == '\n') {
+                expr = expr.substring(0, i) + "\\" + expr.substring(i++);
+            }
+        }
+        return expr;
+    }
+
+    byte[] digest(byte[] manifest, String section) {
+        MessageDigest digester = new EchoMessageDigest();
+        ManifestDigester md = new ManifestDigester(manifest);
+        ManifestDigester.Entry entry = section == null ?
+                md.getMainAttsEntry(oldStyle) : md.get(section, oldStyle);
+        return digestWorkaround ?
+                entry.digestWorkaround(digester) :
+                entry.digest(digester);
+    }
+
+    void test(byte[] originalManifest, boolean mainAttsProperlyDelimited,
+            boolean sectionProperlyDelimited) throws Exception {
+        Utils.echoManifest(originalManifest, "original manifest");
+        byte[] reproducedManifest = reproduceRawManifest(originalManifest,
+                mainAttsProperlyDelimited, sectionProperlyDelimited);
+        Utils.echoManifest(reproducedManifest, "reproduced manifest");
+
+        // The reproduced manifest is not necessarily completely identical to
+        // the original if it contained superfluous blank lines.
+        // It's sufficient that the digests are equal and as an additional
+        // check, the reproduced manifest is here compared to the original
+        // without more than double line breaks.
+        if (!lineBreak.repeat(2).equals(new String(originalManifest, UTF_8))) {
+            assertEquals(
+                new String(reproducedManifest, UTF_8),
+                new String(originalManifest, UTF_8).replaceAll(
+                    regExscape(lineBreak) + "(" + regExscape(lineBreak) + ")+",
+                    lineBreak.repeat(2)));
+        }
+
+        // compare digests of reproduced manifest entries with digests of
+        // original manifest entries
+        assertEquals(digest(originalManifest, null),
+                digest(reproducedManifest, null));
+        for (String key : new Manifest(new ByteArrayInputStream(
+                originalManifest)).getEntries().keySet()) {
+            assertEquals(digest(originalManifest, key),
+                    digest(reproducedManifest, key));
+        }
+
+        // parse and compare original and reproduced manifests as manifests
+        assertEquals(new Manifest(new ByteArrayInputStream(originalManifest)),
+                new Manifest(new ByteArrayInputStream(reproducedManifest)));
+    }
+
+    void test(byte[] originalManifest, boolean mainAttsProperlyDelimited)
+            throws Exception {
+        // assert all individual sections properly delimited particularly useful
+        // when no individual sections present
+        test(originalManifest, mainAttsProperlyDelimited, true);
+    }
+
+    @Test
+    public void testManifestStartsWithBlankLine() throws Exception {
+        test(lineBreak.getBytes(UTF_8), true);
+        test(lineBreak.repeat(2).getBytes(UTF_8), true);
+    }
+
+    @Test
+    public void testEOFAndNoLineBreakAfterMainAttributes() throws Exception {
+        assertThrows(RuntimeException.class, () ->
+            test("Manifest-Version: 1.0".getBytes(UTF_8), false)
+        );
+    }
+
+    @Test
+    public void testEOFAndNoBlankLineAfterMainAttributes() throws Exception {
+        test(("Manifest-Version: 1.0" + lineBreak).getBytes(UTF_8), false);
+    }
+
+    @Test
+    public void testNormalMainAttributes() throws Exception {
+        test(("Manifest-Version: 1.0" +
+                lineBreak.repeat(2)).getBytes(UTF_8), true);
+    }
+
+    @Test
+    public void testExtraLineBreakAfterMainAttributes() throws Exception {
+        test(("Manifest-Version: 1.0" +
+                lineBreak.repeat(3)).getBytes(UTF_8), true);
+    }
+
+    @Test
+    public void testIndividualSectionNoLineBreak() throws Exception {
+        assertNull(new ManifestDigester((
+                "Manifest-Version: 1.0" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value"
+        ).getBytes(UTF_8)).get("Section-Name"));
+    }
+
+    @Test
+    public void testIndividualSectionOneLineBreak() throws Exception {
+        test((
+                "Manifest-Version: 1.0" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak
+        ).getBytes(UTF_8), true, false);
+    }
+
+    @Test
+    public void testNormalIndividualSectionTwoLineBreak() throws Exception {
+        test((
+                "Manifest-Version: 1.0" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak.repeat(2)
+        ).getBytes(UTF_8), true, true);
+    }
+
+    @Test
+    public void testExtraLineBreakAfterIndividualSection() throws Exception {
+        test((
+                "Manifest-Version: 1.0" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak.repeat(3)
+                ).getBytes(UTF_8), true, true);
+    }
+
+    @Test
+    public void testIndividualSections() throws Exception {
+        test((
+                "Manifest-Version: 1.0" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak +
+                lineBreak
+                ).getBytes(UTF_8), true, true);
+    }
+
+    @Test
+    public void testExtraLineBreakBetweenIndividualSections() throws Exception {
+        test((
+                "Manifest-Version: 1.0" + lineBreak +
+                lineBreak +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak +
+                lineBreak.repeat(2) +
+                "Name: Section-Name" + lineBreak +
+                "Key: Value" + lineBreak +
+                lineBreak
+                ).getBytes(UTF_8), true, true);
+    }
+
+}
--- a/test/lib/jdk/test/lib/process/OutputAnalyzer.java	Wed Jul 17 12:26:56 2019 -0300
+++ b/test/lib/jdk/test/lib/process/OutputAnalyzer.java	Thu Jul 18 08:53:06 2019 +0800
@@ -635,10 +635,10 @@
      * just a subset of it.
      *
      * @param from
-     *            The line from where output will be matched.
+     *            The line (excluded) from where output will be matched.
      *            Set {@code from} to null for matching from the first line.
      * @param to
-     *            The line until where output will be matched.
+     *            The line (excluded) until where output will be matched.
      *            Set {@code to} to null for matching until the last line.
      * @param pattern
      *            Matching pattern
@@ -653,10 +653,10 @@
      * just a subset of it.
      *
      * @param from
-     *            The line from where stdout will be matched.
+     *            The line (excluded) from where stdout will be matched.
      *            Set {@code from} to null for matching from the first line.
      * @param to
-     *            The line until where stdout will be matched.
+     *            The line (excluded) until where stdout will be matched.
      *            Set {@code to} to null for matching until the last line.
      * @param pattern
      *            Matching pattern
@@ -670,25 +670,26 @@
 
         int fromIndex = 0;
         if (from != null) {
-            fromIndex = indexOf(lines, from);
-            Asserts.assertGreaterThan(fromIndex, -1,
+            fromIndex = indexOf(lines, from, 0) + 1; // + 1 -> apply 'pattern' to lines after 'from' match
+            Asserts.assertGreaterThan(fromIndex, 0,
                     "The line/pattern '" + from + "' from where the output should match can not be found");
         }
 
         int toIndex = lines.size();
         if (to != null) {
-            toIndex = indexOf(lines, to);
-            Asserts.assertGreaterThan(toIndex, -1,
+            toIndex = indexOf(lines, to, fromIndex);
+            Asserts.assertGreaterThan(toIndex, fromIndex,
                     "The line/pattern '" + to + "' until where the output should match can not be found");
         }
 
         List<String> subList = lines.subList(fromIndex, toIndex);
-        Asserts.assertFalse(subList.isEmpty(), "There are no lines to check");
+        Asserts.assertFalse(subList.isEmpty(), "There are no lines to check:"
+                + " range " + fromIndex + ".." + toIndex + ", subList = " + subList);
 
         subList.stream()
                .filter(Pattern.compile(pattern).asPredicate().negate())
                .findAny()
-               .ifPresent(line -> Asserts.assertTrue(false,
+               .ifPresent(line -> Asserts.fail(
                        "The line '" + line + "' does not match pattern '" + pattern + "'"));
 
         return this;
@@ -698,11 +699,12 @@
      * Check if there is a line matching {@code regexp} and return its index
      *
      * @param regexp Matching pattern
+     * @param fromIndex Start matching after so many lines skipped
      * @return Index of first matching line
      */
-    private int indexOf(List<String> lines, String regexp) {
+    private int indexOf(List<String> lines, String regexp, int fromIndex) {
         Pattern pattern = Pattern.compile(regexp);
-        for (int i = 0; i < lines.size(); i++) {
+        for (int i = fromIndex; i < lines.size(); i++) {
             if (pattern.matcher(lines.get(i)).matches()) {
                 return i;
             }