Modified Getdown to use a signature on the digest.txt file instead of a

signature on the applet parameters, to verify that the signed applet version is
not being misused.
This commit is contained in:
Michael Bayne
2007-04-05 02:10:21 +00:00
parent 19013958c6
commit 707d36aac7
6 changed files with 231 additions and 204 deletions
@@ -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 <code>getdown.txt</code>
* configuration file from the supplied application directory.
@@ -143,11 +161,13 @@ public class Application
* be launched. That application will use <code>appid.class</code> and
* <code>appid.apparg</code> 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.
*
* <p> 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).
*
* <p> 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<String> _jvmargs = new ArrayList<String>();
protected ArrayList<String> _appargs = new ArrayList<String>();
protected Object[] _signers;
protected static final String[] SA_PROTO = new String[0];
}
@@ -80,6 +80,11 @@ public abstract class Getdown extends Thread
}
public Getdown (File appDir, String appId)
{
this(appDir, appId, null);
}
public Getdown (File appDir, String appId, Object[] signers)
{
super("Getdown");
try {
@@ -96,7 +101,7 @@ public abstract class Getdown extends Thread
updateStatus(errmsg);
_dead = true;
}
_app = new Application(appDir, appId);
_app = new Application(appDir, appId, signers);
_startup = System.currentTimeMillis();
}
@@ -23,9 +23,6 @@ package com.threerings.getdown.launcher;
import java.awt.Container;
import java.awt.Image;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.Signature;
import java.security.cert.Certificate;
import javax.swing.JApplet;
import javax.swing.JPanel;
@@ -38,8 +35,6 @@ import java.io.PrintStream;
import netscape.javascript.JSObject;
import netscape.javascript.JSException;
import org.apache.commons.codec.binary.Base64;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
@@ -103,7 +98,10 @@ public class GetdownApplet extends JApplet
}
try {
_getdown = new Getdown(appdir, null) {
// XXX getSigners() returns all certificates used to sign this applet which may allow
// a third party to insert a trusted certificate. This should be replaced with
// statically included trusted keys.
_getdown = new Getdown(appdir, null, GetdownApplet.class.getSigners()) {
protected Container createContainer () {
getContentPane().removeAll();
return getContentPane();
@@ -191,42 +189,6 @@ public class GetdownApplet extends JApplet
}
}
Object[] signers = GetdownApplet.class.getSigners();
if (signers.length == 0) {
Log.info("No signers, not verifying param signature.");
} else {
String signature = getParameter("signature");
if (signature == null) {
signature = "";
}
Log.info("Verifying signature '" + signature + "'.");
String params = appbase + appname + imgpath;
int validated = 0;
for (Object signer : signers) {
if (signer instanceof Certificate) {
Certificate cert = (Certificate)signer;
try {
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(cert);
sig.update(params.getBytes());
if (!sig.verify(Base64.decodeBase64(signature.getBytes()))) {
Log.info("Signature does not match '" + cert.getPublicKey() + "'.");
}
validated++;
} catch (GeneralSecurityException gse) {
// no problem!
}
}
}
// if we couldn't find a key that validates our parameters, we are the hosed!
if (validated == 0) {
throw new Exception("m.corrupt_param_signature_error");
}
}
// pass through properties parameters
String properties = getParameter("properties");
if (properties != null) {
@@ -89,7 +89,7 @@ m.insufficient_permissions_error = You did not accept this application's \
security dialog is shown, click the button to accept the digital signature \
and grant this application the privileges it needs to run.
m.corrupt_param_signature_error = We couldn't verify the application's digital \
m.corrupt_digest_signature_error = We couldn't verify the application's digital \
signature.\nPlease check that you are launching the application from\nthe \
correct website.
@@ -1,128 +0,0 @@
//
// $Id$
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2006 Three Rings Design, Inc.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program; if not, write to the: Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
package com.threerings.getdown.tools;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
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;
/**
* Computes and fills in signed applet parameters. The input file should contain the following
* tokens: <code>@APPBASE@, @APPNAME@, @IMGPATH@, @SIGNATURE@</code> 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 = "";
}
@@ -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;
}