diff --git a/biz.aQute.bndlib/src/aQute/bnd/differ/DiffPluginImpl.java b/biz.aQute.bndlib/src/aQute/bnd/differ/DiffPluginImpl.java index 799b425792..84d09b411e 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/differ/DiffPluginImpl.java +++ b/biz.aQute.bndlib/src/aQute/bnd/differ/DiffPluginImpl.java @@ -1,5 +1,6 @@ package aQute.bnd.differ; +import static aQute.bnd.osgi.Jar.METAINF_SIGNING_P; import static aQute.bnd.service.diff.Delta.CHANGED; import java.io.File; @@ -13,7 +14,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.jar.Manifest; -import java.util.regex.Pattern; import aQute.bnd.header.Attrs; import aQute.bnd.header.OSGiHeader; @@ -129,8 +129,6 @@ private Element bundleElement(Analyzer analyzer) throws Exception { /** * Create an element representing all resources in the JAR */ - private final static Pattern META_INF_P = Pattern.compile("META-INF/([^/]+\\.(MF|SF|DSA|RSA))|(SIG-.*)"); - private Element resourcesElement(Analyzer analyzer) throws Exception { Jar jar = analyzer.getJar(); @@ -139,16 +137,20 @@ private Element resourcesElement(Analyzer analyzer) throws Exception { for (Map.Entry entry : jar.getResources() .entrySet()) { + String path = entry.getKey(); // // The manifest and other (signer) files are ignored // since they are extremely sensitive to time // - if (META_INF_P.matcher(entry.getKey()) - .matches()) + if (jar.getManifestName() + .equals(path) + || METAINF_SIGNING_P.matcher(path) + .matches()) { continue; + } - if (localIgnore != null && localIgnore.matches(entry.getKey())) + if (localIgnore != null && localIgnore.matches(path)) continue; // @@ -159,8 +161,6 @@ private Element resourcesElement(Analyzer analyzer) throws Exception { // directory with source code can be found. // - String path = entry.getKey(); - if (path.endsWith(Constants.EMPTY_HEADER)) continue; diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Builder.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Builder.java index e4db48bc7d..f3ac689792 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Builder.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Builder.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1345,7 +1346,7 @@ private boolean addAll(Jar to, Jar sub, Instruction filter, String destination, boolean dupl = false; for (String name : sub.getResources() .keySet()) { - if ("META-INF/MANIFEST.MF".equals(name)) + if (JarFile.MANIFEST_NAME.equals(name)) continue; if (doNotCopy(Strings.getLastSegment(name, '/'))) diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java index ea35b585df..10ec3fd0f8 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java @@ -41,6 +41,7 @@ import java.util.function.Predicate; import java.util.jar.Attributes; import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.regex.Matcher; @@ -96,7 +97,6 @@ public enum Compression { STORE } - private static final String DEFAULT_MANIFEST_NAME = "META-INF/MANIFEST.MF"; private static final Pattern DEFAULT_DO_NOT_COPY = Pattern .compile(Constants.DEFAULT_DO_NOT_COPY); @@ -106,7 +106,7 @@ public enum Compression { private Optional manifest; private Optional moduleAttribute; private boolean manifestFirst; - private String manifestName = DEFAULT_MANIFEST_NAME; + private String manifestName = JarFile.MANIFEST_NAME; private String name; private File source; private ZipFile zipFile; @@ -122,6 +122,8 @@ public enum Compression { private boolean calculateFileDigest; private int fileLength = -1; private long zipEntryConstantTime = ZIP_ENTRY_CONSTANT_TIME; + public static final Pattern METAINF_SIGNING_P = Pattern + .compile("META-INF/([^/]+\\.(?:DSA|RSA|EC|SF)|SIG-[^/]+)", Pattern.CASE_INSENSITIVE); public Jar(String name) { this.name = name; @@ -590,6 +592,8 @@ public void write(OutputStream to) throws Exception { Set done = new HashSet<>(); Set directories = new HashSet<>(); + + // Write manifest first if (doNotTouchManifest) { Resource r = getResource(manifestName); if (r != null) { @@ -601,6 +605,22 @@ public void write(OutputStream to) throws Exception { done.add(manifestName); } + // Then write any signature info next since JarInputStream really cares! + Map metainf = getDirectory("META-INF"); + if (metainf != null) { + List signing = metainf.keySet() + .stream() + .filter(path -> METAINF_SIGNING_P.matcher(path) + .matches()) + .collect(toList()); + for (String path : signing) { + if (done.add(path)) { + writeResource(jout, directories, path, metainf.get(path)); + } + } + } + + // Write all remaining entries for (Map.Entry entry : getResources().entrySet()) { // Skip metainf contents if (!done.contains(entry.getKey())) @@ -1245,16 +1265,16 @@ public byte[] getTimelessDigest() throws Exception { return md.digest(); } - private final static Pattern SIGNER_FILES_P = Pattern.compile("(.+\\.(SF|DSA|RSA))|(.*/SIG-.*)", - Pattern.CASE_INSENSITIVE); - public void stripSignatures() { - Map map = getDirectory("META-INF"); - if (map != null) { - for (String file : new HashSet<>(map.keySet())) { - if (SIGNER_FILES_P.matcher(file) + Map metainf = getDirectory("META-INF"); + if (metainf != null) { + List signing = metainf.keySet() + .stream() + .filter(path -> METAINF_SIGNING_P.matcher(path) .matches()) - remove(file); + .collect(toList()); + for (String path : signing) { + remove(path); } } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Verifier.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Verifier.java index 6bb8e18199..0b8d7c4f40 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Verifier.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Verifier.java @@ -16,6 +16,7 @@ import java.util.TreeSet; import java.util.function.Function; import java.util.jar.Attributes; +import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1461,7 +1462,7 @@ public void verifyChecksums(boolean all) throws Exception { for (String path : dot.getResources() .keySet()) { - if (path.equals("META-INF/MANIFEST.MF")) + if (path.equals(JarFile.MANIFEST_NAME)) continue; Attributes a = m.getAttributes(path); diff --git a/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java b/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java index 2158ce1034..c42e3506fa 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java +++ b/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java @@ -1,11 +1,13 @@ package aQute.bnd.signing; +import static aQute.bnd.osgi.Jar.METAINF_SIGNING_P; + import java.io.BufferedReader; import java.io.File; import java.io.InputStream; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; +import java.util.jar.JarFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,8 +103,6 @@ public void setRegistry(Registry registry) { processor = registry.getPlugin(Processor.class); } - private static Pattern SIGNING_P = Pattern.compile("META-INF/([^/]*\\.(DSA|RSA|EC|SF|MF)|SIG-[^/]*)"); - @Override public void sign(Builder builder, String alias) throws Exception { File f = builder.getFile(keystore); @@ -112,6 +112,11 @@ public void sign(Builder builder, String alias) throws Exception { } Jar jar = builder.getJar(); + if (!jar.getManifestName() + .equals(JarFile.MANIFEST_NAME)) { + builder.error("Signing requires using the standard manifest name %s", JarFile.MANIFEST_NAME); + return; + } File tmp = File.createTempFile("signedjar", ".jar"); tmp.deleteOnExit(); @@ -188,7 +193,7 @@ public void sign(Builder builder, String alias) throws Exception { builder.addClose(signed); MapStream.of(signed.getDirectory("META-INF")) - .filterKey(path -> SIGNING_P.matcher(path) + .filterKey(path -> JarFile.MANIFEST_NAME.equals(path) || METAINF_SIGNING_P.matcher(path) .matches()) .forEachOrdered(jar::putResource); jar.setDoNotTouchManifest(); diff --git a/biz.aQute.bndlib/src/aQute/bnd/signing/Signer.java b/biz.aQute.bndlib/src/aQute/bnd/signing/Signer.java deleted file mode 100644 index eb8fe1ddf7..0000000000 --- a/biz.aQute.bndlib/src/aQute/bnd/signing/Signer.java +++ /dev/null @@ -1,210 +0,0 @@ -package aQute.bnd.signing; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.Writer; -import java.security.KeyStore; -import java.security.KeyStore.PrivateKeyEntry; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Signature; -import java.util.Map; -import java.util.jar.Manifest; -import java.util.regex.Pattern; - -import aQute.bnd.osgi.EmbeddedResource; -import aQute.bnd.osgi.Jar; -import aQute.bnd.osgi.Processor; -import aQute.bnd.osgi.Resource; -import aQute.lib.base64.Base64; -import aQute.lib.io.ByteBufferOutputStream; -import aQute.lib.io.IO; -import aQute.lib.io.IOConstants; - -/** - * This class is used with the aQute.bnd.osgi package, it signs jars with DSA - * signature. -sign: md5, sha1 - */ -public class Signer extends Processor { - static final int BUFFER_SIZE = IOConstants.PAGE_SIZE * 1; - - private final static Pattern METAINFDIR = Pattern.compile("META-INF/[^/]*"); - String digestNames[] = new String[] { - "MD5" - }; - File keystoreFile = new File("keystore"); - String password; - String alias; - - public void signJar(Jar jar) { - if (digestNames == null || digestNames.length == 0) { - error("Need at least one digest algorithm name, none are specified"); - return; - } - - if (keystoreFile == null || !keystoreFile.getAbsoluteFile() - .exists()) { - error("No such keystore file: %s", keystoreFile); - return; - } - - if (alias == null) { - error("Private key alias not set for signing"); - return; - } - - MessageDigest digestAlgorithms[] = new MessageDigest[digestNames.length]; - - getAlgorithms(digestNames, digestAlgorithms); - - try (ByteBufferOutputStream o = new ByteBufferOutputStream()) { - Manifest manifest = jar.getManifest(); - manifest.getMainAttributes() - .putValue("Signed-By", "Bnd"); - - // Create a new manifest that contains the - // Name parts with the specified digests - - manifest.write(o); - doManifest(jar, digestNames, digestAlgorithms, o); - o.flush(); - byte newManifestBytes[] = o.toByteArray(); - jar.putResource("META-INF/MANIFEST.MF", new EmbeddedResource(newManifestBytes, 0L)); - - // Use the bytes from the new manifest to create - // a signature file - - byte[] signatureFileBytes = doSignatureFile(digestNames, digestAlgorithms, newManifestBytes); - jar.putResource("META-INF/BND.SF", new EmbeddedResource(signatureFileBytes, 0L)); - - // Now we must create an RSA signature - // this requires the private key from the keystore - - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - - KeyStore.PrivateKeyEntry privateKeyEntry = null; - - try (InputStream keystoreInputStream = IO.stream(keystoreFile)) { - char[] pw = password == null ? new char[0] : password.toCharArray(); - - keystore.load(keystoreInputStream, pw); - keystoreInputStream.close(); - privateKeyEntry = (PrivateKeyEntry) keystore.getEntry(alias, new KeyStore.PasswordProtection(pw)); - } catch (Exception e) { - exception(e, "Not able to load the private key from the given keystore(%s) with alias %s", - keystoreFile.getAbsolutePath(), alias); - return; - } - PrivateKey privateKey = privateKeyEntry.getPrivateKey(); - - Signature signature = Signature.getInstance("MD5withRSA"); - signature.initSign(privateKey); - - signature.update(signatureFileBytes); - - signature.sign(); - - // TODO, place the SF in a PCKS#7 structure ... - // no standard class for this? The following - // is an idea but we will to have do ASN.1 BER - // encoding ... - - try (ByteBufferOutputStream tmpStream = new ByteBufferOutputStream()) { - jar.putResource("META-INF/BND.RSA", new EmbeddedResource(tmpStream.toByteBuffer(), 0L)); - } - } catch (Exception e) { - exception(e, "During signing: %s", e); - } - } - - private byte[] doSignatureFile(String[] digestNames, MessageDigest[] algorithms, byte[] manbytes) - throws IOException { - try (ByteBufferOutputStream out = new ByteBufferOutputStream(); PrintWriter ps = IO.writer(out)) { - ps.print("Signature-Version: 1.0\r\n"); - - for (int a = 0; a < algorithms.length; a++) { - if (algorithms[a] != null) { - byte[] digest = algorithms[a].digest(manbytes); - ps.print(digestNames[a] + "-Digest-Manifest: "); - ps.print(new Base64(digest)); - ps.print("\r\n"); - } - } - ps.flush(); - return out.toByteArray(); - } - } - - private void doManifest(Jar jar, String[] digestNames, MessageDigest[] algorithms, OutputStream out) - throws Exception { - Writer w = IO.writer(out, UTF_8); - try { - for (Map.Entry entry : jar.getResources() - .entrySet()) { - String name = entry.getKey(); - if (!METAINFDIR.matcher(name) - .matches()) { - w.write("\r\n"); - w.write("Name: "); - w.write(name); - w.write("\r\n"); - - digest(algorithms, entry.getValue()); - for (int a = 0; a < algorithms.length; a++) { - if (algorithms[a] != null) { - byte[] digest = algorithms[a].digest(); - String header = digestNames[a] + "-Digest: " + new Base64(digest) + "\r\n"; - w.write(header); - } - } - } - } - } finally { - w.flush(); - } - } - - private void digest(MessageDigest[] algorithms, Resource r) throws Exception { - try (InputStream in = r.openInputStream()) { - byte[] data = new byte[BUFFER_SIZE]; - int size = in.read(data); - while (size > 0) { - for (MessageDigest algorithm : algorithms) { - if (algorithm != null) { - algorithm.update(data, 0, size); - } - } - size = in.read(data); - } - } - } - - private void getAlgorithms(String[] digestNames, MessageDigest[] algorithms) { - for (int i = 0; i < algorithms.length; i++) { - String name = digestNames[i]; - try { - algorithms[i] = MessageDigest.getInstance(name); - } catch (NoSuchAlgorithmException e) { - exception(e, "Specified digest algorithm %s, but not such algorithm was found", digestNames[i]); - } - } - } - - public void setPassword(String string) { - password = string; - } - - public void setKeystore(File keystore) { - this.keystoreFile = keystore; - } - - public void setAlias(String string) { - this.alias = string; - } -} diff --git a/biz.aQute.launcher/src/aQute/launcher/Launcher.java b/biz.aQute.launcher/src/aQute/launcher/Launcher.java index 9c335a2ec8..d63f7df990 100644 --- a/biz.aQute.launcher/src/aQute/launcher/Launcher.java +++ b/biz.aQute.launcher/src/aQute/launcher/Launcher.java @@ -63,6 +63,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.Attributes; +import java.util.jar.JarFile; import java.util.jar.Manifest; import org.osgi.framework.Bundle; @@ -221,7 +222,7 @@ public static int main(String[] args, Properties p) throws Throwable { private static String getVersion() { try { Enumeration manifests = Launcher.class.getClassLoader() - .getResources("META-INF/MANIFEST.MF"); + .getResources(JarFile.MANIFEST_NAME); StringBuilder sb = new StringBuilder(); String del = ""; for (Enumeration u = manifests; u.hasMoreElements();) { diff --git a/biz.aQute.launcher/src/aQute/launcher/pre/EmbeddedLauncher.java b/biz.aQute.launcher/src/aQute/launcher/pre/EmbeddedLauncher.java index 875c68080a..5a655c3dbe 100644 --- a/biz.aQute.launcher/src/aQute/launcher/pre/EmbeddedLauncher.java +++ b/biz.aQute.launcher/src/aQute/launcher/pre/EmbeddedLauncher.java @@ -102,8 +102,8 @@ private static T findAndExecute(boolean isVerbose, String methodName, Class< throws Throwable { ClassLoader cl = EmbeddedLauncher.class.getClassLoader(); if (isVerbose) - log("looking for " + EMBEDDED_RUNPATH + " in META-INF/MANIFEST.MF"); - Enumeration manifests = cl.getResources("META-INF/MANIFEST.MF"); + log("looking for " + EMBEDDED_RUNPATH + " in " + JarFile.MANIFEST_NAME); + Enumeration manifests = cl.getResources(JarFile.MANIFEST_NAME); while (manifests.hasMoreElements()) { URL murl = manifests.nextElement(); if (isVerbose) diff --git a/biz.aQute.tester/src/aQute/junit/BundleUtils.java b/biz.aQute.tester/src/aQute/junit/BundleUtils.java index 200c8924bd..17db01be34 100644 --- a/biz.aQute.tester/src/aQute/junit/BundleUtils.java +++ b/biz.aQute.tester/src/aQute/junit/BundleUtils.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.net.URL; import java.util.jar.Attributes; +import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -22,7 +23,7 @@ public static boolean hasNoTests(Bundle bundle) { private static final Attributes.Name TESTCASES = new Attributes.Name(aQute.bnd.osgi.Constants.TESTCASES); public static Stream testCases(Bundle bundle) { - URL url = bundle.getEntry("META-INF/MANIFEST.MF"); + URL url = bundle.getEntry(JarFile.MANIFEST_NAME); if (url == null) { return Stream.empty(); }