diff --git a/src/java/com/threerings/getdown/data/Application.java b/src/java/com/threerings/getdown/data/Application.java
index 50082b0..a409d95 100644
--- a/src/java/com/threerings/getdown/data/Application.java
+++ b/src/java/com/threerings/getdown/data/Application.java
@@ -36,6 +36,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
+import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -43,6 +44,9 @@ import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
+import java.security.GeneralSecurityException;
+import java.security.Signature;
+import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
@@ -58,6 +62,7 @@ import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
+import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import com.threerings.getdown.Log;
@@ -82,6 +87,9 @@ public class Application
* application (minus this prefix). */
public static final String PROP_PASSTHROUGH_PREFIX = "app.";
+ /** Suffix used for control file signatures. */
+ public static final String SIGNATURE_SUFFIX = ".sig";
+
/** Used to communicate information about the UI displayed when updating the application. */
public static class UpdateInterface
{
@@ -135,6 +143,16 @@ public class Application
public void updateStatus (String message);
}
+ /**
+ * Creates an application instance with no signers.
+ *
+ * @see #Application(File, String, Object[])
+ */
+ public Application (File appdir, String appid)
+ {
+ this(appdir, appid, null);
+ }
+
/**
* Creates an application instance which records the location of the getdown.txt
* configuration file from the supplied application directory.
@@ -143,11 +161,13 @@ public class Application
* be launched. That application will use appid.class and
* appid.apparg to configure itself but all other parameters will be the same as
* the primary application.
+ * @param signers an array of possible signers of this application. Used to verify the digest.
*/
- public Application (File appdir, String appid)
+ public Application (File appdir, String appid, Object[] signers)
{
_appdir = appdir;
_appid = appid;
+ _signers = signers;
_config = getLocalPath(CONFIG_FILE);
}
@@ -650,7 +670,7 @@ public class Application
// now re-download our control files; we download the digest first so that if it fails, our
// config file will still reference the old version and re-running the updater will start
// the whole process over again
- downloadControlFile(Digest.DIGEST_FILE);
+ downloadControlFile(Digest.DIGEST_FILE, true);
downloadControlFile(CONFIG_FILE);
}
@@ -835,7 +855,7 @@ public class Application
String olddig = (_digest == null) ? "" : _digest.getMetaDigest();
try {
status.updateStatus("m.checking");
- downloadControlFile(Digest.DIGEST_FILE);
+ downloadControlFile(Digest.DIGEST_FILE, true);
_digest = new Digest(_appdir);
if (!olddig.equals(_digest.getMetaDigest())) {
Log.info("Unversioned digest changed. Revalidating...");
@@ -853,7 +873,7 @@ public class Application
// exceptions to propagate up to the caller as there is nothing else we can do
if (_digest == null) {
status.updateStatus("m.updating_metadata");
- downloadControlFile(Digest.DIGEST_FILE);
+ downloadControlFile(Digest.DIGEST_FILE, true);
_digest = new Digest(_appdir);
}
@@ -864,7 +884,7 @@ public class Application
// attempt to redownload both of our metadata files; again we pass errors up to our
// caller because there's nothing we can do to automatically recover
downloadControlFile(CONFIG_FILE);
- downloadControlFile(Digest.DIGEST_FILE);
+ downloadControlFile(Digest.DIGEST_FILE, true);
_digest = new Digest(_appdir);
// revalidate everything if we end up downloading new metadata
clearValidationMarkers();
@@ -985,13 +1005,114 @@ public class Application
}
/**
- * Downloads a new copy of the specified control file and, if the download is successful, moves
- * it over the old file on the filesystem.
+ * Downloads a new copy of the specified control file and does not check any signature.
*/
protected void downloadControlFile (String path)
throws IOException
+ {
+ downloadControlFile(path, false);
+ }
+
+ /**
+ * Downloads a new copy of the specified control file, optionally validating its signature.
+ * If the download is successful, moves it over the old file on the filesystem.
+ *
+ *
We implement simple signing of the digest.txt file for use with the Getdown applet, but + * this should never be used as-is with a non-applet getdown installation, as the signing + * format has no provisions for declaring arbitrary signing key IDs, signature algorithm, et al + * -- it is entirely reliant on the ability to upgrade the Getdown applet, and its signature + * validation implementation, at-will (ie, via an Applet). + * + *
TODO: Switch to PKCS #7 or CMS.
+ */
+ protected void downloadControlFile (String path, boolean validateSignature)
+ throws IOException
+ {
+ File target = downloadFile(path);
+
+ if (validateSignature) {
+ if (_signers == null) {
+ Log.info("No signers, not verifying file [path=" + path + "].");
+
+ } else {
+ File signatureFile = downloadFile(path + SIGNATURE_SUFFIX);
+ byte[] signature = null;
+ try {
+ signature = IOUtils.toByteArray(new FileReader(signatureFile), "utf8");
+ } finally {
+ // delete the file regardless
+ signatureFile.delete();
+ }
+
+ FileInputStream dataInput = new FileInputStream(target);
+ byte[] buffer = new byte[8192];
+ int length, validated = 0;
+ for (Object signer : _signers) {
+ if (!(signer instanceof Certificate)) {
+ continue;
+ }
+
+ Certificate cert = (Certificate)signer;
+ try {
+ Signature sig = Signature.getInstance("SHA1withRSA");
+ sig.initVerify(cert);
+ while ((length = dataInput.read(buffer)) != -1) {
+ sig.update(buffer, 0, length);
+ }
+
+ if (!sig.verify(Base64.decodeBase64(signature))) {
+ Log.info("Signature does not match [cert=" + cert.getPublicKey() + "]");
+ continue;
+ } else {
+ Log.info("Signature matches [cert=" + cert.getPublicKey() + "]");
+ validated++;
+ }
+
+ } catch (GeneralSecurityException gse) {
+ // no problem!
+ }
+ }
+
+ // if we couldn't find a key that validates our digest, we are the hosed!
+ if (validated == 0) {
+ // delete the temporary digest file as we know it is invalid
+ target.delete();
+ throw new IOException("m.corrupt_digest_signature_error");
+ }
+ }
+ }
+
+ // Windows is a wonderful operating system, it won't let you rename a file overtop of
+ // another one; thus to avoid running the risk of getting royally fucked, we have to do
+ // this complicated backup bullshit; this way if the shit hits the fan before we get the
+ // new copy into place, we should be able to read from the backup copy; yay!
+ File original = getLocalPath(path);
+ if (RunAnywhere.isWindows() && original.exists()) {
+ File backup = getLocalPath(path + "_old");
+ if (backup.exists() && !backup.delete()) {
+ Log.warning("Failed to delete " + backup + ".");
+ }
+ if (!original.renameTo(backup)) {
+ Log.warning("Failed to move " + original + " to backup. We will likely fail " +
+ "to replace it with " + target + ".");
+ }
+ }
+
+ // now attempt to replace the current file with the new one
+ if (!target.renameTo(original)) {
+ throw new IOException("Failed to rename(" + target + ", " + original + ")");
+ }
+ }
+
+ /**
+ * Download a path to a temporary file, returning a {@link File} instance with the path
+ * contents.
+ */
+ protected File downloadFile (String path)
+ throws IOException
{
File target = getLocalPath(path + "_new");
+
URL targetURL = null;
try {
targetURL = getRemoteURL(path);
@@ -1015,26 +1136,7 @@ public class Application
StreamUtil.close(fout);
}
- // Windows is a wonderful operating system, it won't let you rename a file overtop of
- // another one; thus to avoid running the risk of getting royally fucked, we have to do
- // this complicated backup bullshit; this way if the shit hits the fan before we get the
- // new copy into place, we should be able to read from the backup copy; yay!
- File original = getLocalPath(path);
- if (RunAnywhere.isWindows() && original.exists()) {
- File backup = getLocalPath(path + "_old");
- if (backup.exists() && !backup.delete()) {
- Log.warning("Failed to delete " + backup + ".");
- }
- if (!original.renameTo(backup)) {
- Log.warning("Failed to move " + original + " to backup. We will likely fail " +
- "to replace it with " + target + ".");
- }
- }
-
- // now attempt to replace the current file with the new one
- if (!target.renameTo(original)) {
- throw new IOException("Failed to rename(" + target + ", " + original + ")");
- }
+ return target;
}
/** Helper function for creating {@link Resource} instances. */
@@ -1131,5 +1233,7 @@ public class Application
protected ArrayList@APPBASE@, @APPNAME@, @IMGPATH@, @SIGNATURE@ which will be filled in based
- * on supplied and computed values.
- */
-public class AppletParamTask extends Task
-{
- public void setAppbase (String appbase)
- {
- _appbase = appbase;
- }
-
- public void setAppname (String appname)
- {
- _appname = appname;
- }
-
- public void setImgpath (String imgpath)
- {
- _imgpath = imgpath;
- }
-
- public void setKeystore (File keystore)
- {
- _keystore = keystore;
- }
-
- public void setStorepass (String storepass)
- {
- _storepass = storepass;
- }
-
- public void setAlias (String alias)
- {
- _alias = alias;
- }
-
- public void setKeypass (String keypass)
- {
- _keypass = keypass;
- }
-
- public void setFile (File file)
- {
- _input = file;
- }
-
- public void setTofile (File tofile)
- {
- _output = tofile;
- }
-
- public void execute () throws BuildException
- {
- String params = _appbase + _appname + _imgpath;
-
- try {
- KeyStore store = KeyStore.getInstance("JKS");
- store.load(new BufferedInputStream(new FileInputStream(_keystore)),
- _storepass.toCharArray());
- PrivateKey key = (PrivateKey)store.getKey(_alias, _keypass.toCharArray());
- Signature sig = Signature.getInstance("SHA1withRSA");
- sig.initSign(key);
- sig.update(params.getBytes());
- String signature = new String(Base64.encodeBase64(sig.sign()));
-
- BufferedReader bin = new BufferedReader(new FileReader(_input));
- PrintWriter pout = new PrintWriter(new FileWriter(_output));
- String line;
- while ((line = bin.readLine()) != null) {
- line = line.replaceAll("@APPNAME@", _appname);
- line = line.replaceAll("@APPBASE@", _appbase);
- line = line.replaceAll("@IMGPATH@", _imgpath);
- line = line.replaceAll("@SIGNATURE@", signature);
- pout.println(line);
- }
-
- bin.close();
- pout.close();
-
- } catch (Exception e) {
- throw new BuildException(e);
- }
- }
-
- protected String _appbase, _appname, _imgpath;
- protected File _keystore, _input, _output;
- protected String _storepass = "", _alias, _keypass = "";
-}
diff --git a/src/java/com/threerings/getdown/tools/DigesterTask.java b/src/java/com/threerings/getdown/tools/DigesterTask.java
index a1aaca4..92c2a6a 100644
--- a/src/java/com/threerings/getdown/tools/DigesterTask.java
+++ b/src/java/com/threerings/getdown/tools/DigesterTask.java
@@ -21,9 +21,17 @@
package com.threerings.getdown.tools;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.Signature;
+
+import org.apache.commons.codec.binary.Base64;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
@@ -45,6 +53,30 @@ public class DigesterTask extends Task
_appdir = appdir;
}
+ /**
+ * Sets the digest signing keystore.
+ */
+ public void setKeystore (File path)
+ {
+ _storepath = path;
+ }
+
+ /**
+ * Sets the keystore decryption key.
+ */
+ public void setStorepass (String password)
+ {
+ _storepass = password;
+ }
+
+ /**
+ * Sets the private key alias.
+ */
+ public void setAlias (String alias)
+ {
+ _storealias = alias;
+ }
+
/**
* Performs the actual work of the task.
*/
@@ -52,16 +84,27 @@ public class DigesterTask extends Task
{
// make sure appdir is set
if (_appdir == null) {
- String errmsg = "Must specify the path to the application " +
- "directory via the 'appdir' attribute.";
- throw new BuildException(errmsg);
+ throw new BuildException("Must specify the path to the application directory " +
+ "via the 'appdir' attribute.");
+ }
+
+ // make sure _storepass and _keyalias are set, if _storepath is set
+ if (_storepath != null) {
+ if (_storepass == null || _storealias == null) {
+ throw new BuildException(
+ "Must specify both a keystore password and a private key alias.");
+ }
}
try {
createDigest(_appdir);
+ if (_storepath != null) {
+ signDigest(_appdir, _storepath, _storepass, _storealias);
+ }
} catch (IOException ioe) {
- throw new BuildException("Error creating digest: " +
- ioe.getMessage(), ioe);
+ throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe);
+ } catch (GeneralSecurityException gse) {
+ throw new BuildException("Error creating signature: " + gse.getMessage(), gse);
}
}
@@ -90,6 +133,47 @@ public class DigesterTask extends Task
Digest.createDigest(rsrcs, target);
}
+ /**
+ * Creates a digest file in the specified application directory.
+ */
+ public static void signDigest (File appdir, File storePath, String storePass, String storeAlias)
+ throws IOException, GeneralSecurityException
+ {
+ File inputFile = new File(appdir, Digest.DIGEST_FILE);
+ File signatureFile = new File(appdir, Digest.DIGEST_FILE + Application.SIGNATURE_SUFFIX);
+
+ // initialize the keystore
+ KeyStore store = KeyStore.getInstance("JKS");
+ FileInputStream storeInput = new FileInputStream(storePath);
+ store.load(storeInput, storePass.toCharArray());
+ PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray());
+
+ // sign the digest file
+ Signature sig = Signature.getInstance("SHA1withRSA");
+ FileInputStream dataInput = new FileInputStream(inputFile);
+ byte[] buffer = new byte[8192];
+ int length;
+
+ sig.initSign(key);
+ while ((length = dataInput.read(buffer)) != -1) {
+ sig.update(buffer, 0, length);
+ }
+
+ // Write out the signature
+ FileOutputStream signatureOutput = new FileOutputStream(signatureFile);
+ String signed = new String(Base64.encodeBase64(sig.sign()));
+ signatureOutput.write(signed.getBytes("utf8"));
+ }
+
/** The application directory in which we're creating a digest file. */
protected File _appdir;
+
+ /** The path to the keystore we'll use to sign the digest file, if any. */
+ protected File _storepath;
+
+ /** The decryption key for the keystore. */
+ protected String _storepass;
+
+ /** The private key alias. */
+ protected String _storealias;
}