package aQute.bnd.signing; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayOutputStream; 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.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; 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"); 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 { Manifest manifest = jar.getManifest(); manifest.getMainAttributes().putValue("Signed-By", "Bnd"); // Create a new manifest that contains the // Name parts with the specified digests ByteArrayOutputStream o = new ByteArrayOutputStream(); manifest.write(o); doManifest(jar, digestNames, digestAlgorithms, o); o.flush(); byte newManifestBytes[] = o.toByteArray(); jar.putResource("META-INF/MANIFEST.MF", new EmbeddedResource(newManifestBytes, 0)); // 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, 0)); // 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 ... ByteArrayOutputStream tmpStream = new ByteArrayOutputStream(); jar.putResource("META-INF/BND.RSA", new EmbeddedResource(tmpStream.toByteArray(), 0)); } catch (Exception e) { exception(e, "During signing: %s", e); } } private byte[] doSignatureFile(String[] digestNames, MessageDigest[] algorithms, byte[] manbytes) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); 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"); } } 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<String,Resource> 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 (int a = 0; a < algorithms.length; a++) { if (algorithms[a] != null) { algorithms[a].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; } }