diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java index 057ad2ef40dcc4c727134dab460735a91ce03ef1..0299156a20a2e445317833357392eb263f1ceba5 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import net.i2p.crypto.TrustedUpdate; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.router.RouterVersion; @@ -136,7 +137,7 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { String ver = buf.substring(index+VERSION_PREFIX.length(), end); if (_log.shouldLog(Log.DEBUG)) _log.debug("Found version: [" + ver + "]"); - if (needsUpdate(ver)) { + if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Our version is out of date, update!"); break; @@ -191,54 +192,6 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { } } - private boolean needsUpdate(String version) { - StringTokenizer newTok = new StringTokenizer(sanitize(version), "."); - StringTokenizer ourTok = new StringTokenizer(sanitize(RouterVersion.VERSION), "."); - - while (newTok.hasMoreTokens() && ourTok.hasMoreTokens()) { - String newVer = newTok.nextToken(); - String oldVer = ourTok.nextToken(); - switch (compare(newVer, oldVer)) { - case -1: // newVer is smaller - return false; - case 0: // eq - break; - case 1: // newVer is larger - return true; - } - } - if (newTok.hasMoreTokens() && !ourTok.hasMoreTokens()) - return true; - return false; - } - - private static final String VALID = "0123456789."; - private static final String sanitize(String str) { - StringBuffer buf = new StringBuffer(str); - for (int i = 0; i < buf.length(); i++) { - if (VALID.indexOf(buf.charAt(i)) == -1) { - buf.deleteCharAt(i); - i--; - } - } - return buf.toString(); - } - - private static final int compare(String lhs, String rhs) { - try { - int left = Integer.parseInt(lhs); - int right = Integer.parseInt(rhs); - if (left < right) - return -1; - else if (left == right) - return 0; - else - return 1; - } catch (NumberFormatException nfe) { - return 0; - } - } - public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { // ignore } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java index c2fd838f5b2f47b2aa3fff853b354b07fe3fa185..c412d025b31f59a0585bf9cc6b5a483d9114c05f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java @@ -2,19 +2,25 @@ package net.i2p.router.web; import java.io.File; import java.text.DecimalFormat; + import net.i2p.crypto.TrustedUpdate; import net.i2p.router.Router; import net.i2p.router.RouterContext; -import net.i2p.util.I2PThread; +import net.i2p.router.RouterVersion; import net.i2p.util.EepGet; +import net.i2p.util.I2PThread; import net.i2p.util.Log; /** - * Handle the request to update the router by firing off an EepGet call and - * displaying its status to anyone who asks. After the download completes, - * it is verified with the TrustedUpdate, and if it is authentic, the router - * is restarted. - * + * <p>Handles the request to update the router by firing off an + * {@link net.i2p.util.EepGet} call to download the latest signed update file + * and displaying the status to anyone who asks. + * </p> + * <p>After the download completes the signed update file is verified with + * {@link net.i2p.crypto.TrustedUpdate}, and if it's authentic the payload + * of the signed update file is unpacked and the router is restarted to complete + * the update process. + * </p> */ public class UpdateHandler { private static UpdateRunner _updateRunner; @@ -140,7 +146,7 @@ public class UpdateHandler { public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile) { _status = "<b>Update downloaded</b><br />"; TrustedUpdate up = new TrustedUpdate(_context); - boolean ok = up.migrateVerified(SIGNED_UPDATE_FILE, "i2pupdate.zip"); + boolean ok = up.migrateVerified(RouterVersion.VERSION, SIGNED_UPDATE_FILE, "i2pupdate.zip"); File f = new File(SIGNED_UPDATE_FILE); f.delete(); if (ok) { diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java index c801628a9c149640e279a18585988afc73c58af4..acd4bfc964b97b68ce5ef1bea6b5becd46ffc998 100644 --- a/core/java/src/net/i2p/crypto/TrustedUpdate.java +++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java @@ -1,14 +1,15 @@ package net.i2p.crypto; -import java.io.File; -import java.io.FileNotFoundException; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.SequenceInputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.StringTokenizer; +import net.i2p.CoreVersion; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; @@ -18,15 +19,28 @@ import net.i2p.data.SigningPublicKey; import net.i2p.util.Log; /** - * Handles DSA signing and verification of I2P update archives. + * <p>Handles DSA signing and verification of update files. + * </p> + * <p>For convenience this class also makes certain operations available via the + * command line. These can be invoked as follows: + * </p> + * <pre> + * java net.i2p.crypto.TrustedUpdate keygen <i>publicKeyFile privateKeyFile</i> + * java net.i2p.crypto.TrustedUpdate showversion <i>signedFile</i> + * java net.i2p.crypto.TrustedUpdate sign <i>inputFile signedFile privateKeyFile version</i> + * java net.i2p.crypto.TrustedUpdate verifysig <i>signedFile</i> + * java net.i2p.crypto.TrustedUpdate verifyupdate <i>signedFile</i> + * </pre> * - * @author smeghead + * @author jrandom and smeghead */ public class TrustedUpdate { + /** - * default trusted key, generated by jrandom. This can be authenticated - * via gpg without modification (gpg --verify TrustedUpdate.java) - * + * <p>Default trusted key generated by jrandom@i2p.net. This can be + * authenticated via <code>gpg</code> without modification:</p> + * <p> + * <code>gpg --verify TrustedUpdate.java</code></p> */ /* -----BEGIN PGP SIGNED MESSAGE----- @@ -47,334 +61,575 @@ CPah6TDXYJCWmR0n3oPtrvo= -----END PGP SIGNATURE----- */ - private ArrayList _trustedKeys; + private static final String VALID_VERSION_CHARS = "0123456789."; + private static final int VERSION_BYTES = 16; + private static final int HEADER_BYTES = Signature.SIGNATURE_BYTES + VERSION_BYTES; + private static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys"; + + private static I2PAppContext _context; - private I2PAppContext _context; - private Log _log; + private Log _log; + private ArrayList _trustedKeys; - private static final int VERSION_BYTES = 16; - private static final int HEADER_BYTES = VERSION_BYTES + Signature.SIGNATURE_BYTES; - - public static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys"; - + /** + * Constructs a new <code>TrustedUpdate</code> with the default global + * context. + */ public TrustedUpdate() { this(I2PAppContext.getGlobalContext()); } - public TrustedUpdate(I2PAppContext ctx) { - _context = ctx; + + /** + * Constructs a new <code>TrustedUpdate</code> with the given + * {@link net.i2p.I2PAppContext}. + * + * @param context An instance of <code>I2PAppContext</code>. + */ + public TrustedUpdate(I2PAppContext context) { + _context = context; _log = _context.logManager().getLog(TrustedUpdate.class); - _trustedKeys = new ArrayList(1); - String keys = ctx.getProperty(PROP_TRUSTED_KEYS); - if ( (keys != null) && (keys.length() > 0) ) { - StringTokenizer tok = new StringTokenizer(keys, ", "); - while (tok.hasMoreTokens()) - _trustedKeys.add(tok.nextToken()); + _trustedKeys = new ArrayList(); + + String propertyTrustedKeys = context.getProperty(PROP_TRUSTED_KEYS); + + if ( (propertyTrustedKeys != null) && (propertyTrustedKeys.length() > 0) ) { + StringTokenizer propertyTrustedKeysTokens = new StringTokenizer(propertyTrustedKeys, ","); + + while (propertyTrustedKeysTokens.hasMoreTokens()) + _trustedKeys.add(propertyTrustedKeysTokens.nextToken().trim()); + } else { _trustedKeys.add(DEFAULT_TRUSTED_KEY); } } - - public ArrayList getTrustedKeys() { return _trustedKeys; } - - public static void main(String[] args) { - if (args.length <= 0) { - usage(); - } else if ("keygen".equals(args[0])) { - genKeysCLI(args[1], args[2]); - } else if ("sign".equals(args[0])) { - signCLI(args[1], args[2], args[3], args[4]); - } else if ("verify".equals(args[0])) { - verifyCLI(args[1]); - } else { - usage(); + + /** + * Parses command line arguments when this class is used from the command + * line. + * + * @param args Command line parameters. + */ + public static void main(String[] args) { + try { + if ("keygen".equals(args[0])) { + genKeysCLI(args[1], args[2]); + } else if ("showversion".equals(args[0])) { + showVersionCLI(args[1]); + } else if ("sign".equals(args[0])) { + signCLI(args[1], args[2], args[3], args[4]); + } else if ("verifysig".equals(args[0])) { + verifySigCLI(args[1]); + } else if ("verifyupdate".equals(args[0])) { + verifyUpdateCLI(args[1]); + } else { + showUsageCLI(); + } + } catch (ArrayIndexOutOfBoundsException aioobe) { + showUsageCLI(); + } + } + + /** + * Checks if the given version is newer than the given current version. + * + * @param currentVersion The current version. + * @param newVersion The version to test. + * + * @return <code>true</code> if the given version is newer than the current + * version, otherwise <code>false</code>. + */ + public static final boolean needsUpdate(String currentVersion, String newVersion) { + StringTokenizer newVersionTokens = new StringTokenizer(sanitize(newVersion), "."); + StringTokenizer currentVersionTokens = new StringTokenizer(sanitize(currentVersion), "."); + + while (newVersionTokens.hasMoreTokens() && currentVersionTokens.hasMoreTokens()) { + String newNumber = newVersionTokens.nextToken(); + String currentNumber = currentVersionTokens.nextToken(); + + switch (compare(newNumber, currentNumber)) { + case -1: // newNumber is smaller + return false; + case 0: // eq + break; + case 1: // newNumber is larger + return true; + } } - } - private static final void usage() { - System.err.println("Usage: TrustedUpdate keygen publicKeyFile privateKeyFile"); - System.err.println(" TrustedUpdate sign origFile signedFile privateKeyFile version"); - System.err.println(" TrustedUpdate verify signedFile"); + if (newVersionTokens.hasMoreTokens() && !currentVersionTokens.hasMoreTokens()) + return true; + + return false; } - + + private static final int compare(String lop, String rop) { + try { + int left = Integer.parseInt(lop); + int right = Integer.parseInt(rop); + + if (left < right) + return -1; + else if (left == right) + return 0; + else + return 1; + } catch (NumberFormatException nfe) { + return 0; + } + } + private static final void genKeysCLI(String publicKeyFile, String privateKeyFile) { - FileOutputStream out = null; + FileOutputStream fileOutputStream = null; + try { - I2PAppContext ctx = I2PAppContext.getGlobalContext(); - Object keys[] = ctx.keyGenerator().generateSigningKeypair(); - SigningPublicKey pub = (SigningPublicKey)keys[0]; - SigningPrivateKey priv = (SigningPrivateKey)keys[1]; - - out = new FileOutputStream(publicKeyFile); - pub.writeBytes(out); - out.close(); - - out = new FileOutputStream(privateKeyFile); - priv.writeBytes(out); - out.close(); - out = null; - System.out.println("Private keys writen to " + privateKeyFile + " and public to " + publicKeyFile); - System.out.println("Public: " + pub.toBase64()); + Object signingKeypair[] = _context.keyGenerator().generateSigningKeypair(); + SigningPublicKey signingPublicKey = (SigningPublicKey) signingKeypair[0]; + SigningPrivateKey signingPrivateKey = (SigningPrivateKey) signingKeypair[1]; + + fileOutputStream = new FileOutputStream(publicKeyFile); + signingPublicKey.writeBytes(fileOutputStream); + fileOutputStream.close(); + fileOutputStream = null; + + fileOutputStream = new FileOutputStream(privateKeyFile); + signingPrivateKey.writeBytes(fileOutputStream); + + System.out.println("\r\nPrivate key written to: " + privateKeyFile); + System.out.println("Public key written to: " + publicKeyFile); + System.out.println("\r\nPublic key: " + signingPublicKey.toBase64() + "\r\n"); } catch (Exception e) { + System.err.println("Error writing keys:"); e.printStackTrace(); - System.err.println("Error writing out the keys"); } finally { - if (out != null) try { out.close(); } catch (IOException ioe) {} + if (fileOutputStream != null) + try { + fileOutputStream.close(); + } catch (IOException ioe) { + } + } + } + + private static final String sanitize(String versionString) { + StringBuffer versionStringBuffer = new StringBuffer(versionString); + + for (int i = 0; i < versionStringBuffer.length(); i++) { + if (VALID_VERSION_CHARS.indexOf(versionStringBuffer.charAt(i)) == -1) { + versionStringBuffer.deleteCharAt(i); + i--; + } } + + return versionStringBuffer.toString(); } - private static final void signCLI(String origFile, String outFile, String privKeyFile, String version) { - TrustedUpdate up = new TrustedUpdate(); - Signature sig = up.sign(origFile, outFile, privKeyFile, version); - if (sig != null) - System.out.println("Signed and written to " + outFile); + private static final void showUsageCLI() { + System.err.println("Usage: TrustedUpdate keygen publicKeyFile privateKeyFile"); + System.err.println(" TrustedUpdate showversion signedFile"); + System.err.println(" TrustedUpdate sign inputFile signedFile privateKeyFile version"); + System.err.println(" TrustedUpdate verifysig signedFile"); + System.err.println(" TrustedUpdate verifyupdate signedFile"); + } + + private static final void showVersionCLI(String signedFile) { + String versionString = new TrustedUpdate().getVersionString(signedFile); + + if (versionString == "") + System.out.println("No version string found in file '" + signedFile + "'"); else - System.out.println("Error signing"); + System.out.println("Version: " + versionString); } - - private static final void verifyCLI(String signedFile) { - TrustedUpdate up = new TrustedUpdate(); - boolean ok = up.verify(signedFile); - if (ok) + + private static final void signCLI(String inputFile, String signedFile, String privateKeyFile, String version) { + Signature signature = new TrustedUpdate().sign(inputFile, signedFile, privateKeyFile, version); + + if (signature != null) + System.out.println("Input file '" + inputFile + "' signed and written to '" + signedFile + "'"); + else + System.out.println("Error signing input file '" + inputFile + "'"); + } + + private static final void verifySigCLI(String signedFile) { + boolean isValidSignature = new TrustedUpdate().verify(signedFile); + + if (isValidSignature) System.out.println("Signature VALID"); else System.out.println("Signature INVALID"); } - /** - * Reads the version string from a signed I2P update file. - * - * @param inputFile A signed I2P update file. - * - * @return The update version string read, or an empty string if no version - * string is present. - */ - public String getUpdateVersion(String inputFile) { - FileInputStream in = null; + private static final void verifyUpdateCLI(String signedFile) { + boolean isUpdate = new TrustedUpdate().isUpdatedVersion(CoreVersion.VERSION, signedFile); + + if (isUpdate) + System.out.println("File version is newer than current version."); + else + System.out.println("File version is older than or equal to current version."); + } + + /** + * Fetches the trusted keys for the current instance. + * + * @return An <code>ArrayList</code> containting the trusted keys. + */ + public ArrayList getTrustedKeys() { + return _trustedKeys; + } + + /** + * Reads the version string from a signed update file. + * + * @param signedFile A signed update file. + * + * @return The version string read, or an empty string if no version string + * is present. + */ + public String getVersionString(String signedFile) { + FileInputStream fileInputStream = null; + try { - in = new FileInputStream(inputFile); - byte data[] = new byte[VERSION_BYTES]; - int read = DataHelper.read(in, data); - if (read != VERSION_BYTES) - return null; + fileInputStream = new FileInputStream(signedFile); + byte[] data = new byte[VERSION_BYTES]; + int bytesRead = DataHelper.read(fileInputStream, data, Signature.SIGNATURE_BYTES, VERSION_BYTES); + + if (bytesRead != VERSION_BYTES) + return ""; + for (int i = 0; i < VERSION_BYTES; i++) if (data[i] == 0x00) return new String(data, 0, i, "UTF-8"); + return new String(data, "UTF-8"); } catch (UnsupportedEncodingException uee) { - // If this ever gets called, you need a new JVM. throw new RuntimeException("wtf, your JVM doesnt support utf-8? " + uee.getMessage()); - } catch (IOException ioe) { + } catch (IOException ioe) { return ""; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - } - - /** - * Uses the given private key to sign the given input file with DSA. The - * output will be a binary file where the first 16 bytes are the I2P - * update's version string encoded in UTF-8 (padded with trailing - * <code>0h</code> characters if necessary), the next 40 bytes are the - * resulting DSA signature, and the remaining bytes are the input file. - * - * @param inputFile The file to be signed. - * @param outputFile The signed file to write. - * @param privateKeyFile The name of the file containing the private key to - * sign <code>inputFile</code> with. - * @param updateVersion The version number of the I2P update. If this - * string is longer than 16 characters it will be - * truncated. - * - * @return An instance of {@link net.i2p.data.Signature}, or null if there was an error - */ - public Signature sign(String inputFile, String outputFile, String privateKeyFile, String updateVersion) { - SigningPrivateKey key = new SigningPrivateKey(); - FileInputStream in = null; + } + + /** + * Verifies that the version of the given signed update file is newer than + * <code>currentVersion</code>. + * + * @param currentVersion The current version to check against. + * @param signedFile The signed update file. + * + * @return <code>true</code> if the signed update file's version is newer + * than the current version, otherwise <code>false</code>. + */ + public boolean isUpdatedVersion(String currentVersion, String signedFile) { + if (needsUpdate(currentVersion, getVersionString(signedFile))) + return true; + else + return false; + } + + /** + * Verifies the signature of a signed update file, and if it's valid and the + * file's version is newer than the given current version, migrates the data + * out of <code>signedFile</code> and into <code>outputFile</code>. + * + * @param currentVersion The current version to check against. + * @param signedFile A signed update file. + * @param outputFile The file to write the verified data to. + * + * @return <code>true</code> if the signature and version were valid and the + * data was moved, <code>false</code> otherwise. + */ + public boolean migrateVerified(String currentVersion, String signedFile, String outputFile) { + if (!isUpdatedVersion(currentVersion, signedFile)) + return false; + + if (!verify(signedFile)) + return false; + + FileInputStream fileInputStream = null; + FileOutputStream fileOutputStream = null; + try { - in = new FileInputStream(privateKeyFile); - key.readBytes(in); + fileInputStream = new FileInputStream(signedFile); + fileOutputStream = new FileOutputStream(outputFile); + long skipped = 0; + + while (skipped < HEADER_BYTES) + skipped += fileInputStream.skip(HEADER_BYTES - skipped); + + byte[] buffer = new byte[1024]; + int bytesRead = 0; + + while ( (bytesRead = fileInputStream.read(buffer)) != -1) + fileOutputStream.write(buffer, 0, bytesRead); + } catch (IOException ioe) { + return false; + } finally { + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } + + if (fileOutputStream != null) + try { + fileOutputStream.close(); + } catch (IOException ioe) { + } + } + + return true; + } + + /** + * Uses the given private key to sign the given input file along with its + * version string using DSA. The output will be a signed update file where + * the first 40 bytes are the resulting DSA signature, the next 16 bytes are + * the input file's version string encoded in UTF-8 (padded with trailing + * <code>0h</code> characters if necessary), and the remaining bytes are the + * raw bytes of the input file. + * + * @param inputFile The file to be signed. + * @param signedFile The signed update file to write. + * @param privateKeyFile The name of the file containing the private key to + * sign <code>inputFile</code> with. + * @param version The version string of the input file. If this is + * longer than 16 characters it will be truncated. + * + * @return An instance of {@link net.i2p.data.Signature}, or + * <code>null</code> if there was an error. + */ + public Signature sign(String inputFile, String signedFile, String privateKeyFile, String version) { + FileInputStream fileInputStream = null; + SigningPrivateKey signingPrivateKey = new SigningPrivateKey(); + + try { + fileInputStream = new FileInputStream(privateKeyFile); + signingPrivateKey.readBytes(fileInputStream); } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signing key", ioe); + return null; } catch (DataFormatException dfe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signing key", dfe); + return null; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - - return sign(inputFile, outputFile, key, updateVersion); + + return sign(inputFile, signedFile, signingPrivateKey, version); } - - public Signature sign(String inputFile, String outputFile, SigningPrivateKey privKey, String updateVersion) { - byte[] headerUpdateVersion = { - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 }; - byte[] updateVersionBytes = null; - if (updateVersion.length() > VERSION_BYTES) - updateVersion = updateVersion.substring(0, VERSION_BYTES); - try { - updateVersionBytes = updateVersion.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - // If this ever gets called, you need a new JVM. + + /** + * Uses the given {@link net.i2p.data.SigningPrivateKey} to sign the given + * input file along with its version string using DSA. The output will be a + * signed update file where the first 40 bytes are the resulting DSA + * signature, the next 16 bytes are the input file's version string encoded + * in UTF-8 (padded with trailing <code>0h</code> characters if necessary), + * and the remaining bytes are the raw bytes of the input file. + * + * @param inputFile The file to be signed. + * @param signedFile The signed update file to write. + * @param signingPrivateKey An instance of <code>SigningPrivateKey</code> + * to sign <code>inputFile</code> with. + * @param version The version string of the input file. If this is + * longer than 16 characters it will be truncated. + * + * @return An instance of {@link net.i2p.data.Signature}, or + * <code>null</code> if there was an error. + */ + public Signature sign(String inputFile, String signedFile, SigningPrivateKey signingPrivateKey, String version) { + byte[] versionHeader = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + byte[] versionRawBytes = null; + + if (version.length() > VERSION_BYTES) + version = version.substring(0, VERSION_BYTES); + + try { + versionRawBytes = version.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { throw new RuntimeException("wtf, your JVM doesnt support utf-8? " + e.getMessage()); - } - System.arraycopy(updateVersionBytes, 0, headerUpdateVersion, 0, updateVersionBytes.length); + } + System.arraycopy(versionRawBytes, 0, versionHeader, 0, versionRawBytes.length); + + FileInputStream fileInputStream = null; Signature signature = null; - FileInputStream in = null; + SequenceInputStream bytesToSignInputStream = null; + ByteArrayInputStream versionHeaderInputStream = null; + try { - in = new FileInputStream(inputFile); - signature = _context.dsa().sign(in, privKey); + fileInputStream = new FileInputStream(inputFile); + versionHeaderInputStream = new ByteArrayInputStream(versionHeader); + bytesToSignInputStream = new SequenceInputStream(versionHeaderInputStream, fileInputStream); + signature = _context.dsa().sign(bytesToSignInputStream, signingPrivateKey); + } catch (Exception e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error signing", e); + return null; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - in = null; + if (bytesToSignInputStream != null) + try { + bytesToSignInputStream.close(); + } catch (IOException ioe) { + } + + fileInputStream = null; } + FileOutputStream fileOutputStream = null; + try { - fileOutputStream = new FileOutputStream(outputFile); - fileOutputStream.write(headerUpdateVersion); - fileOutputStream.write(signature.getData()); - - in = new FileInputStream(inputFile); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - fileOutputStream.write(buf, 0, read); - fileOutputStream.close(); - fileOutputStream = null; - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.log(Log.WARN, "Error writing signed I2P update file " + outputFile, ioe); + fileOutputStream = new FileOutputStream(signedFile); + fileOutputStream.write(signature.getData()); + fileOutputStream.write(versionHeader); + fileInputStream = new FileInputStream(inputFile); + byte[] buffer = new byte[1024]; + int bytesRead = 0; + while ( (bytesRead = fileInputStream.read(buffer)) != -1) + fileOutputStream.write(buffer, 0, bytesRead); + fileOutputStream.close(); + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.log(Log.WARN, "Error writing signed file " + signedFile, ioe); + return null; - } finally { - if (fileOutputStream != null) try { fileOutputStream.close(); } catch (IOException ioe) {} - if (in != null) try { in.close(); } catch (IOException ioe) {} + } finally { + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } + + if (fileOutputStream != null) + try { + fileOutputStream.close(); + } catch (IOException ioe) { + } } - return signature; - } - - /** - * Verifies the DSA signature of a signed I2P update. - * - * @param inputFile The signed update file to check. - * - * @return <code>true</code> if the file has a valid signature. - */ - public boolean verify(String inputFile) { + return signature; + } + + /** + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * + * @return <code>true</code> if the file has a valid signature, otherwise + * <code>false</code>. + */ + public boolean verify(String signedFile) { for (int i = 0; i < _trustedKeys.size(); i++) { - SigningPublicKey key = new SigningPublicKey(); + SigningPublicKey signingPublicKey = new SigningPublicKey(); + try { - key.fromBase64((String)_trustedKeys.get(i)); - boolean ok = verify(inputFile, key); - if (ok) return true; + signingPublicKey.fromBase64((String)_trustedKeys.get(i)); + boolean isValidSignature = verify(signedFile, signingPublicKey); + + if (isValidSignature) + return true; } catch (DataFormatException dfe) { _log.log(Log.CRIT, "Trusted key " + i + " is not valid"); } } + if (_log.shouldLog(Log.WARN)) _log.warn("None of the keys match"); + return false; } - - /** - * Verifies the DSA signature of a signed I2P update. - * - * @param inputFile The signed update file to check. - * @param key public key to verify against - * - * @return <code>true</code> if the file has a valid signature. - */ - public boolean verify(String inputFile, SigningPublicKey key) { - FileInputStream in = null; - try { - in = new FileInputStream(inputFile); - byte version[] = new byte[VERSION_BYTES]; - Signature sig = new Signature(); - if (VERSION_BYTES != DataHelper.read(in, version)) - throw new IOException("Not enough data for the version bytes"); - sig.readBytes(in); - return _context.dsa().verifySignature(sig, in, key); - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error reading " + inputFile + " to verify", ioe); - return false; - } catch (DataFormatException dfe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error reading the signature", dfe); - return false; - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - } - } - - /** - * Verifies the DSA signature of a signed I2P update. - * - * @param inputFile The signed update file to check. - * @param publicKeyFile The public key to use for verification. - * - * @return <code>true</code> if the file has a valid signature. - */ - public boolean verify(String inputFile, String publicKeyFile) { - SigningPublicKey pub = new SigningPublicKey(); - FileInputStream in = null; + + /** + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * @param publicKeyFile A file containing the public key to use for + * verification. + * + * @return <code>true</code> if the file has a valid signature, otherwise + * <code>false</code>. + */ + public boolean verify(String signedFile, String publicKeyFile) { + SigningPublicKey signingPublicKey = new SigningPublicKey(); + FileInputStream fileInputStream = null; + try { - in = new FileInputStream(inputFile); - pub.readBytes(in); + fileInputStream = new FileInputStream(signedFile); + signingPublicKey.readBytes(fileInputStream); } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signature", ioe); + return false; } catch (DataFormatException dfe) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to load the signature", dfe); + return false; } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - - return verify(inputFile, pub); - } - + + return verify(signedFile, signingPublicKey); + } + /** - * Verify the signature on the signed inputFile, and if it is valid, migrate - * the raw data out of it and into the outputFile - * - * @return true if the signature was valid and the data moved, false otherwise. + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * @param signingPublicKey An instance of + * {@link net.i2p.data.SigningPublicKey} to use for + * verification. + * + * @return <code>true</code> if the file has a valid signature, otherwise + * <code>false</code>. */ - public boolean migrateVerified(String inputFile, String outputFile) { - boolean ok = verify(inputFile); - if (!ok) return false; - FileOutputStream out = null; - FileInputStream in = null; + public boolean verify(String signedFile, SigningPublicKey signingPublicKey) { + FileInputStream fileInputStream = null; + try { - out = new FileOutputStream(outputFile); - in = new FileInputStream(inputFile); - long skipped = 0; - while (skipped < HEADER_BYTES) { - skipped += in.skip(HEADER_BYTES - skipped); - } - - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); + fileInputStream = new FileInputStream(signedFile); + Signature signature = new Signature(); + + signature.readBytes(fileInputStream); + + return _context.dsa().verifySignature(signature, fileInputStream, signingPublicKey); } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error reading " + signedFile + " to verify", ioe); + + return false; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error reading the signature", dfe); + return false; } finally { - if (out != null) try { out.close(); } catch (IOException ioe) {} - if (in != null) try { in.close(); } catch (IOException ioe) {} + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException ioe) { + } } - return true; } } diff --git a/history.txt b/history.txt index 91ac6030a66f610f02b8685b755b27e94f30403d..3488ff706991d037fc62a6fd84e7b36df9ad4346 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,14 @@ -$Id: history.txt,v 1.188 2005/04/05 17:24:32 jrandom Exp $ +$Id: history.txt,v 1.189 2005/04/06 10:43:25 jrandom Exp $ + +2005-04-08 smeghead + * Security improvements to TrustedUpdate: signing and verification of the + version string along with the data payload for signed update files + (consequently the positions of the DSA signature and version string fields + have been swapped in the spec for the update file's header); router will + no longer perform a trusted update if the signed update's version is lower + than or equal to the currently running router's version. + * Added two new CLI commands to TrustedUpdate: showversion, verifyupdate. + * Extended TrustedUpdate public API for use by third party applications. * 2005-04-06 0.5.0.6 released