From eaaed88fd9fe98a8a986c9c828e6050a38d9f6d6 Mon Sep 17 00:00:00 2001 From: BJ Hargrave Date: Fri, 27 May 2022 21:44:05 -0400 Subject: [PATCH 1/4] signing: Remove old dead code Signed-off-by: BJ Hargrave --- .../src/aQute/bnd/signing/Signer.java | 210 ------------------ 1 file changed, 210 deletions(-) delete mode 100644 biz.aQute.bndlib/src/aQute/bnd/signing/Signer.java 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; - } -} From a1bb0eafb27b10bd69edd437896fa61b7cd8d9e0 Mon Sep 17 00:00:00 2001 From: BJ Hargrave Date: Sat, 28 May 2022 16:26:11 -0400 Subject: [PATCH 2/4] signing: Check for non-standard manifest name Signing is only possible when using the standard manifest name. Signed-off-by: BJ Hargrave --- biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java b/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java index 2158ce1034..d3f24ca02f 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java +++ b/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java @@ -6,6 +6,7 @@ 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; @@ -112,6 +113,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(); From 922b0a9c64f0c9db6284877842bf53561bce6796 Mon Sep 17 00:00:00 2001 From: BJ Hargrave Date: Sat, 28 May 2022 16:27:44 -0400 Subject: [PATCH 3/4] manifest: Use string constant for standard manifest name Signed-off-by: BJ Hargrave --- biz.aQute.bndlib/src/aQute/bnd/osgi/Builder.java | 3 ++- biz.aQute.bndlib/src/aQute/bnd/osgi/Verifier.java | 3 ++- biz.aQute.launcher/src/aQute/launcher/Launcher.java | 3 ++- .../src/aQute/launcher/pre/EmbeddedLauncher.java | 4 ++-- biz.aQute.tester/src/aQute/junit/BundleUtils.java | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) 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/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.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(); } From 37e7d2dd0430563845eaa5f80ae3f786d32191cd Mon Sep 17 00:00:00 2001 From: BJ Hargrave Date: Sat, 28 May 2022 16:33:11 -0400 Subject: [PATCH 4/4] signing: Write META-INF signing resources immediately after manifest JarInputStream requires the META-INF signing resources to come after the manifest and before any other resources. Otherwise, JarInputStream does not consider the jar to be properly signed. Later versions of Equinox now use JarInputStream to verify the jar signing replacing Equinox's custom verification code. So Bnd needs to properly generate signed jars to work with JarInputStream. We also define a standard pattern for the META-INF signing resources which is used by other classes that care. Signed-off-by: BJ Hargrave --- .../src/aQute/bnd/differ/DiffPluginImpl.java | 16 ++++---- biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java | 40 ++++++++++++++----- .../src/aQute/bnd/signing/JartoolSigner.java | 7 ++-- 3 files changed, 41 insertions(+), 22 deletions(-) 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/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/signing/JartoolSigner.java b/biz.aQute.bndlib/src/aQute/bnd/signing/JartoolSigner.java index d3f24ca02f..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,12 @@ 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; @@ -102,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); @@ -194,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();