Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
Cryptography Cryptography Tutorial Java Java and Cryptography Mastery Series — Extended Module 15 - Digital Signatures and PKI

Java Program Using jarsigner to Sign JAR Files

Reviewed & accurate
AI Summary

Introduction

The jarsigner tool was introduced in JDK 1.2 in December 1998 alongside keytool to give Java applet authors a way to digitally sign JAR files so the browser plug-in could grant elevated permissions to trusted code. Twenty-five years later, applets are gone but jarsigner remains the production tool that signs every Android APK (through the apksigner wrapper), every JAR published to Maven Central, and every JMod and JLink runtime image. The Java JAR File Specification defines exactly three signature-related files that jarsigner writes: META-INF/MANIFEST.MF with per-entry SHA-256 digests, META-INF/SIGNERNAME.SF with per-entry digest-of-digest, and META-INF/SIGNERNAME.RSA or .DSA or .EC with the PKCS7 SignedData blob. A Java learner should care because the same algorithm runs every time Maven, Gradle, or the Android Gradle Plugin produces a release artifact.

This tutorial invokes the real jarsigner binary through ProcessBuilder to sign a freshly built JAR, then immediately invokes jarsigner -verify to confirm the signature. It then loads the JAR through java.util.jar.JarFile and walks the META-INF entries to show how a verifier would extract the per-entry digests and check them against the actual file contents.

What is jarsigner?

jarsigner is a JDK-supplied command-line tool that signs and verifies JAR files using a keystore created by keytool. Signing a JAR means computing a cryptographic digest (SHA-256 by default) of every file inside the JAR, recording those digests in the manifest, signing the manifest digests with a private key, and embedding the resulting PKCS7 signature in a binary file in META-INF. Verifying a JAR means recomputing every file's digest, comparing against the manifest, recomputing the manifest digests, comparing against the .SF file, and finally validating the PKCS7 signature against a trusted certificate.

jarsigner is fundamentally different from a normal MAC like HMAC: it is an asymmetric, public-key signature. Anyone with the public certificate can verify; only the holder of the private key can sign. That is what makes the Maven Central infrastructure possible: thousands of consumers can independently verify that the JAR they downloaded really was published by the stated maintainer.

Since JDK 13 the JDK also exposes a programmatic JarSigner API in the jdk.security.jarsigner package, but the command-line jarsigner binary remains the canonical way to sign from build scripts because it accepts the same -keystore and -storepass arguments as keytool.

How jarsigner Works - Step by Step

Step 1: Build the manifest. jarsigner scans every file inside the JAR (except files already in META-INF), reads each one as a binary stream, computes the SHA-256 digest, base64-encodes the 32-byte digest, and writes the line Name: path/to/file followed by SHA-256-Digest: base64digest into META-INF/MANIFEST.MF. The manifest also gets a Created-By header so consumers know which tool version produced the file.

Step 2: Build the signature file (.SF). For each manifest entry, jarsigner computes the SHA-256 digest of the manifest section's text (the Name line plus all attribute lines plus the trailing newline), base64-encodes that, and writes Name: path/to/file plus SHA-256-Digest: base64 into META-INF/ALIAS.SF. The .SF file also gets a top-level SHA-256-Digest-Manifest attribute that is the digest of the entire MANIFEST.MF file. That top-level digest lets a verifier short-circuit: if it matches, the per-entry check can be skipped.

Step 3: Sign the .SF file. jarsigner reads the .SF bytes, asks the keystore for the private key under the requested alias, computes a PKCS7 SignedData structure (CMS signedData with the certificate chain, the signature algorithm SHA256withRSA, and the signature bytes), and writes the binary blob into META-INF/ALIAS.RSA (or .DSA for DSA keys, or .EC for EC keys).

Step 4: Verify. jarsigner -verify foo.jar extracts the .RSA block, recovers the certificate chain, validates it against the JVM trust store, recovers the .SF file using the public key from the certificate, recomputes the top-level SHA-256-Digest-Manifest and compares against the manifest, then walks each manifest entry to recompute and compare digests. If any step fails, the JAR is rejected.

Step 5: Timestamp. jarsigner -tsa https://freetsa.org sends an RFC 3161 timestamp query to the named Time Stamping Authority, embeds the TSA's countersignature inside the PKCS7 SignedData, and writes the result into META-INF. Without a timestamp, the signature becomes invalid the moment the signing certificate expires, which would break every JAR signed more than a year ago.

Java Program for jarsigner

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

public class JarsignerDemo {

    // Locations of JDK binaries, derived from java.home.
    static final String JARSIGNER = System.getProperty("java.home")
            + File.separator + "bin" + File.separator + "jarsigner";
    static final String KEYTOOL = System.getProperty("java.home")
            + File.separator + "bin" + File.separator + "keytool";

    public static void main(String[] args) throws Exception {
        String ksPath = "signer.p12";
        String storePass = "changeit";
        String alias = "signer";
        String jarPath = "demo.jar";
        String dn = "CN=IMCSEIAN Signer, O=IMCSEIAN, C=IN";

        // Step 1: create a real keystore with a real RSA keypair.
        String[] genkeypair = {
            KEYTOOL,
            "-genkeypair",
            "-alias", alias,
            "-keyalg", "RSA",
            "-keysize", "2048",
            "-sigalg", "SHA256withRSA",
            "-validity", "365",
            "-dname", dn,
            "-keystore", ksPath,
            "-storetype", "PKCS12",
            "-storepass", storePass
        };
        System.out.println("==> keytool -genkeypair");
        System.out.println(run(genkeypair));

        // Step 2: build a tiny demo JAR with two class-like entries.
        try (JarOutputStream jos = new JarOutputStream(
                new FileOutputStream(jarPath), new Manifest())) {
            writeEntry(jos, "com/imcseian/Main.class",
                    "fake class bytes".getBytes("UTF-8"));
            writeEntry(jos, "com/imcseian/Util.class",
                    "more fake bytes".getBytes("UTF-8"));
        }
        System.out.println("==> built demo JAR: " + jarPath);

        // Step 3: sign the JAR with jarsigner.
        String[] signCmd = {
            JARSIGNER,
            "-keystore", ksPath,
            "-storepass", storePass,
            "-signedjar", "signed.jar",
            jarPath,
            alias
        };
        System.out.println("==> jarsigner -sign");
        System.out.println(run(signCmd));

        // Step 4: verify the signed JAR.
        String[] verifyCmd = {
            JARSIGNER,
            "-verify",
            "-verbose",
            "-keystore", ksPath,
            "-storepass", storePass,
            "signed.jar"
        };
        System.out.println("==> jarsigner -verify");
        System.out.println(run(verifyCmd));

        // Step 5: walk the META-INF entries of the signed JAR to show the
        // signature files that jarsigner produced.
        try (JarFile jf = new JarFile("signed.jar")) {
            System.out.println("==> META-INF entries of signed.jar");
            java.util.Enumeration<JarEntry> en = jf.entries();
            while (en.hasMoreElements()) {
                JarEntry e = en.nextElement();
                if (e.getName().startsWith("META-INF/")) {
                    System.out.printf("  %-40s %d bytes%n",
                            e.getName(), e.getSize());
                }
            }
        }

        // Cleanup.
        new File(ksPath).delete();
        new File(jarPath).delete();
        new File("signed.jar").delete();
    }

    static void writeEntry(JarOutputStream jos, String name, byte[] data)
            throws Exception {
        JarEntry e = new JarEntry(name);
        jos.putNextEntry(e);
        jos.write(data);
        jos.closeEntry();
    }

    static String run(String[] cmd) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(cmd);
        pb.redirectErrorStream(true);
        Process p = pb.start();
        StringBuilder out = new StringBuilder();
        try (BufferedReader r = new BufferedReader(
                new InputStreamReader(p.getInputStream()))) {
            String line;
            while ((line = r.readLine()) != null) {
                out.append(line).append('\n');
            }
        }
        int code = p.waitFor();
        if (code != 0) {
            out.append("Exit code: ").append(code).append('\n');
        }
        return out.toString();
    }
}

Code Explanation - Line by Line

The first half of the program is identical to the keytool tutorial: it locates the JDK binaries under java.home/bin, runs keytool -genkeypair to create a PKCS12 keystore, and captures stdout. The run() helper merges stderr into stdout so a non-zero exit code still surfaces a readable error.

Step 2 builds a tiny demo JAR using java.util.jar.JarOutputStream. The constructor takes a Manifest that becomes the seed of META-INF/MANIFEST.MF; the seed manifest is empty here so jarsigner can add its own attributes later without conflict. The writeEntry() helper wraps each payload in a JarEntry, which the JAR format stores as a compressed (deflate) region by default.

Step 3 invokes the actual jarsigner binary with the same kind of argument array that an Ant or Gradle build would pass. The -signedjar flag tells jarsigner to write the signed output to a new file rather than overwriting the input. The alias argument is the last positional parameter and identifies the keystore entry whose private key will be used.

Step 4 invokes jarsigner -verify -verbose on the signed JAR, which prints a line per JAR entry showing the digest status (smk where s means the signature covers the entry, m means the manifest entry is present, k means the signature verifies at least once with a certificate in the keystore). Finally, step 5 walks the META-INF entries through JarFile.entries() to show the SIGNER.SF and SIGNER.RSA files that jarsigner wrote alongside the existing MANIFEST.MF.

Sample Input and Output

==> keytool -genkeypair
Generated 2048-bit RSA key pair and self-signed certificate (SHA256withRSA)
with a validity of 365 days for: CN=IMCSEIAN Signer, O=IMCSEIAN, C=IN

==> built demo JAR: demo.jar

==> jarsigner -sign
jar signed.

Warning: no -tsa option was used. The signer certificate will expire
on ... After that date the JAR may become unverifiable.

==> jarsigner -verify
s k      0 Fri ... com/imcseian/Main.class
s k      0 Fri ... com/imcseian/Util.class
...

jar verified.

==> META-INF entries of signed.jar
  META-INF/MANIFEST.MF                     124 bytes
  META-INF/SIGNER.SF                        89 bytes
  META-INF/SIGNER.RSA                      2048 bytes

The output demonstrates the three signature files that the JAR specification mandates: MANIFEST.MF with per-entry digests, SIGNER.SF with per-entry digest-of-digest plus the top-level SHA-256-Digest-Manifest, and SIGNER.RSA with the PKCS7 SignedData blob. The "no -tsa" warning is expected because we deliberately did not call a Timestamp Authority for this short demo.

Common Mistakes to Avoid

First, never sign JARs without a TSA timestamp. A certificate is typically valid for 1 year (public) or 3 years (private CA); without an RFC 3161 timestamp countersignature, the JAR becomes unverifiable the moment the certificate expires, even though the JAR was signed while the certificate was valid. Maven Central rejects unsigned JARs and warns on timestampless JARs. Second, do not reuse the same keystore for multiple projects if the projects have different maintainers: each maintainer should have their own alias so consumers can identify the signer. Third, do not assume that jarsigner -verify foo.jar without a -keystore argument means the signature is trustworthy: without -keystore, jarsigner only verifies that the signature is mathematically correct, not that the certificate is trusted. Use jarsigner -verify -strict -keystore ... for full validation. Finally, do not forget to add the digest algorithm explicitly on JDK 17+ with -digestalg SHA-256 if you must support very old JREs that understand only SHA-1.

Real-World Applications

Maven Central requires every published JAR to be signed with PGP, not jarsigner, but internal enterprise Nexus and Artifactory repositories often require jarsigner signatures because they integrate with the JVM trust store directly. The Android Gradle Plugin's release variant runs apksigner (a thin wrapper around jarsigner plus the v2/v3 APK signature schemes) to produce the signed APK that gets uploaded to Google Play. Spring Boot executable JARs are not signed by default, but enterprise security teams often add a jarsigner step in the CI pipeline to attest that the JAR passed the company's SAST scan.

The Java Web Start tool that ran sandboxed JNLP applications relied on jarsigner signatures to decide whether to grant the JAR elevated permissions; Web Start is end-of-lifed in JDK 11, but the same trust model survives in the OpenWebStart open-source reimplementation that still uses jarsigner-signed JARs for enterprise desktop deployment.

Conclusion

We saw that jarsigner writes three files (MANIFEST.MF, .SF, .RSA) that together encode a detached PKCS7 signature over the SHA-256 digests of every JAR entry, and that the JDK ships a programmatic JarFile API that can read those files in three lines of code. Next, study the RSA Digital Signature tutorial to learn how the PKCS7 SignedData blob is built, then read the X.509 Certificate Parsing tutorial to understand the certificate chain that jarsigner embeds inside the .RSA file.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments