This is the answer I came to after reading and experimentation.
In this case, a self-signed certificate may be sufficient, since no third-party code needs to authenticate. The hosting code can use the public key to verify the plug-in.
Details
The examples below use the default key algorithm. You may wish to specify a more secure algorithm with -keyalg
.
Make the keystore, a public/private key pair, and a self-signed certificate containing the public key
keytool -genkeypair -alias myalias -validity 365 -keystore mykeystore
Validity is measured in days.
Export the certificate containing the public key
keytool -export -keystore mykeystore -alias myalias -file mycertificate.cer
At build time, sign the plug-in jar
jarsigner -keystore mykeystore -signedjar my_signed.jar my_unsigned.jar myalias
At run time, authenticate the plug-in jar contents
This test harness can be used to test the code that follows.
public class CEVerify {
public static void main(String[] args) throws IOException, CertificateException {
File jarFile = new File( "C:\\myplugins\\my_signed.jar" );
String certificatePath = "C:\\mycertificates\\mycertificate.cer";
File certificateFile = new File( certificatePath );
PublicKey publicKey = getPublicKeyFromCertificate( certificateFile );
JarFile jar = new JarFile( jarFile );
boolean isVerified = verify( jar, publicKey );
if ( isVerified ) {
System.out.println( "Verified!" );
}
else {
System.err.println( "NOT verified!" );
}
}
You can extract the public key from the certificate like this:
private static PublicKey
getPublicKeyFromCertificate( File certificateFile )
throws CertificateException, FileNotFoundException
{
CertificateFactory certificateFactory = CertificateFactory.getInstance( "X.509" );
FileInputStream inCertificate = new FileInputStream( certificateFile );
Certificate certificate = certificateFactory.generateCertificate( inCertificate );
return certificate.getPublicKey();
}
Given a jar file and a public key, you can verify appropriate entries in the jar. You may need to exclude other files if you used a different -keyalg
, like RSA.
private static boolean
verify( JarFile jar, PublicKey publicKey ) throws IOException {
Enumeration<JarEntry> jarEntries = jar.entries();
while ( jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if ( jarEntry.isDirectory()) {
continue;
}
else {
String entryName = jarEntry.getName();
if ( entryName.endsWith( ".SF" ) || entryName.endsWith( ".DSA" )) {
continue;
}
else if ( ! verifyJarEntry( jar, publicKey, jarEntry )) {
return false;
}
}
}
return true;
}
And this authenticates a particular file in a jar. Note the need to read all the bytes in the jar entry before its certificates can be obtained.
private static boolean
verifyJarEntry( JarFile jar, PublicKey publicKey, JarEntry jarEntry)
throws IOException
{
try {
InputStream in = jar.getInputStream( jarEntry );
readAllOf( in );
// public Certificate[] getCertificates()
// ... This method can only be called once the JarEntry has been
// completely verified by reading from the entry input stream
// until the end of the stream has been reached. Otherwise, this
// method will return null.
Certificate[] certificates = jarEntry.getCertificates();
if ((null == certificates) || (0 == certificates.length)) {
return false;
} else {
for (int i = 0; i < certificates.length; ++i) {
Certificate certificate = certificates[i];
try {
certificate.verify( publicKey );
return true;
} catch (Exception e) {
continue;
}
}
return false;
}
} catch (SecurityException e) {
return false;
}
}
Finally, this is the method called above to read all the bytes in a jar entry.
private static void readAllOf(InputStream in) throws IOException {
byte[] buffer = new byte[4096];
while ( 0 < in.read( buffer )) {
continue;
}
}